feat: 优化用户任务页面和管理页面的表格样式
调整字体大小和列宽,增强可读性;更新管理员登录页面的样式,调整按钮和文本大小;修复考试科目页面、考试任务页面、问题管理页面等多个页面的表格分页样式,统一设置为小尺寸;更新选项列表和测验组件的样式,提升用户体验。
This commit is contained in:
BIN
data/survey.db
BIN
data/survey.db
Binary file not shown.
@@ -4,7 +4,7 @@
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/src/assets/正方形LOGO.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>宝来威天问平台</title>
|
||||
<title>宝来威考试平台</title>
|
||||
<meta name="description" content="功能完善的在线问卷调查系统,支持多种题型、随机抽题、免注册答题等特性" />
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -68,7 +68,7 @@
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.container {
|
||||
padding: 0 16px;
|
||||
padding: 0px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
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;
|
||||
|
||||
@@ -97,12 +98,12 @@ const AdminLayout = ({ children }: { children: React.ReactNode }) => {
|
||||
collapsible
|
||||
collapsed={collapsed}
|
||||
theme="light"
|
||||
className="shadow-md z-10"
|
||||
className="shadow-md z-10 fixed left-0 top-0 h-screen overflow-y-auto"
|
||||
width={240}
|
||||
>
|
||||
<div className="h-16 flex items-center justify-center border-b border-gray-100">
|
||||
{collapsed ? (
|
||||
<span className="text-xl font-bold text-mars-500">OA</span>
|
||||
<img src={正方形LOGO} alt="正方形LOGO" style={{ width: '24px', height: '32px' }} />
|
||||
) : (
|
||||
<img src={主要LOGO} alt="主要LOGO" style={{ width: '180px', height: '72px' }} />
|
||||
)}
|
||||
@@ -117,7 +118,7 @@ const AdminLayout = ({ children }: { children: React.ReactNode }) => {
|
||||
/>
|
||||
</Sider>
|
||||
|
||||
<Layout className="bg-gray-50/50">
|
||||
<Layout className="bg-gray-50/50 ml-0 transition-all duration-300" style={{ marginLeft: collapsed ? 80 : 240 }}>
|
||||
<Header className="bg-white shadow-sm flex justify-between items-center px-6 h-16 sticky top-0 z-10">
|
||||
<Button
|
||||
type="text"
|
||||
@@ -139,18 +140,16 @@ const AdminLayout = ({ children }: { children: React.ReactNode }) => {
|
||||
</div>
|
||||
</Header>
|
||||
|
||||
<Content className="m-6 flex flex-col">
|
||||
<div className="flex-1 bg-white rounded-xl shadow-sm p-6 min-h-[calc(100vh-160px)]">
|
||||
{children}
|
||||
</div>
|
||||
<Content className="m-6 flex flex-col pb-20">
|
||||
{children}
|
||||
</Content>
|
||||
|
||||
<Footer className="bg-transparent text-center py-6 px-8 text-gray-400 text-sm flex flex-col md:flex-row justify-between items-center">
|
||||
<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>
|
||||
© {new Date().getFullYear()} Boonlive OA System. All Rights Reserved.
|
||||
</div>
|
||||
<div className="mt-4 md:mt-0">
|
||||
<img src={纯字母LOGO} alt="纯字母LOGO" style={{ width: '136px', height: '51px' }} />
|
||||
<div className="mt-2 md:mt-0">
|
||||
<img src={纯字母LOGO} alt="纯字母LOGO" style={{ width: '100px', height: '38px' }} />
|
||||
</div>
|
||||
</Footer>
|
||||
</Layout>
|
||||
|
||||
@@ -2,15 +2,21 @@ import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import { ConfigProvider } from 'antd';
|
||||
import zhCN from 'antd/locale/zh_CN';
|
||||
import dayjs from 'dayjs';
|
||||
import 'dayjs/locale/zh-cn';
|
||||
import { UserProvider, AdminProvider, QuizProvider } from './contexts';
|
||||
import App from './App';
|
||||
import 'antd/dist/reset.css';
|
||||
import './index.css';
|
||||
|
||||
dayjs.locale('zh-cn');
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<React.StrictMode>
|
||||
<BrowserRouter>
|
||||
<ConfigProvider
|
||||
locale={zhCN}
|
||||
theme={{
|
||||
token: {
|
||||
colorPrimary: '#008C8C',
|
||||
@@ -44,6 +50,9 @@ ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
headerBg: '#ffffff',
|
||||
siderBg: '#ffffff',
|
||||
},
|
||||
Pagination: {
|
||||
size: 'small',
|
||||
},
|
||||
}
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -120,18 +120,18 @@ const HomePage = () => {
|
||||
|
||||
return (
|
||||
<Layout className="min-h-screen bg-gray-50">
|
||||
<Header className="bg-white shadow-sm flex items-center px-6 h-16 sticky top-0 z-10">
|
||||
<img src={主要LOGO} alt="主要LOGO" style={{ width: '180px', height: '72px' }} />
|
||||
<Header className="bg-white shadow-sm flex items-center px-4 h-12 sticky top-0 z-10">
|
||||
<img src={主要LOGO} alt="主要LOGO" style={{ width: '120px', height: '48px' }} />
|
||||
</Header>
|
||||
|
||||
<Content className="flex items-center justify-center p-4 bg-gradient-to-br from-mars-50 to-white">
|
||||
<Content className="flex items-center justify-center p-2 bg-gradient-to-br from-mars-50 to-white">
|
||||
<div className="w-full max-w-md">
|
||||
<Card className="shadow-xl border-t-4 border-t-mars-500">
|
||||
<div className="text-center mb-8">
|
||||
<Title level={2} className="text-mars-600 !mb-2">
|
||||
宝来威天问平台
|
||||
<Card className="shadow-xl border-t-4 border-t-mars-500 p-2">
|
||||
<div className="text-center mb-3">
|
||||
<Title level={2} className="text-mars-600 !mb-0.5 !text-lg">
|
||||
宝来威考试平台
|
||||
</Title>
|
||||
<p className="text-gray-500">
|
||||
<p className="text-gray-500 text-xs">
|
||||
请填写您的基本信息开始答题
|
||||
</p>
|
||||
</div>
|
||||
@@ -141,7 +141,6 @@ const HomePage = () => {
|
||||
layout="vertical"
|
||||
onFinish={handleSubmit}
|
||||
autoComplete="off"
|
||||
size="large"
|
||||
>
|
||||
<Form.Item
|
||||
label="姓名"
|
||||
@@ -151,6 +150,7 @@ const HomePage = () => {
|
||||
{ min: 2, max: 20, message: '姓名长度必须在2-20个字符之间' },
|
||||
{ pattern: /^[\u4e00-\u9fa5a-zA-Z\s]+$/, message: '姓名只能包含中文、英文和空格' }
|
||||
]}
|
||||
className="mb-2"
|
||||
>
|
||||
<AutoComplete
|
||||
options={historyOptions}
|
||||
@@ -170,6 +170,7 @@ const HomePage = () => {
|
||||
{ required: true, message: '请输入手机号' },
|
||||
{ pattern: /^1[3-9]\d{9}$/, message: '请输入正确的中国手机号' }
|
||||
]}
|
||||
className="mb-2"
|
||||
>
|
||||
<Input
|
||||
placeholder="请输入11位手机号"
|
||||
@@ -183,6 +184,7 @@ const HomePage = () => {
|
||||
rules={[
|
||||
{ required: true, message: '请输入登录密码' }
|
||||
]}
|
||||
className="mb-2"
|
||||
>
|
||||
<Input.Password
|
||||
placeholder="请输入登录密码"
|
||||
@@ -191,23 +193,23 @@ const HomePage = () => {
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item className="mb-2 mt-8">
|
||||
<Form.Item className="mb-2 mt-3">
|
||||
<Button
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
loading={loading}
|
||||
block
|
||||
className="bg-mars-500 hover:!bg-mars-600 border-none shadow-md h-12 text-lg font-medium"
|
||||
className="bg-mars-500 hover:!bg-mars-600 border-none shadow-md h-9 text-sm font-medium"
|
||||
>
|
||||
开始答题
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
|
||||
<div className="mt-6 text-center">
|
||||
<div className="mt-3 text-center">
|
||||
<a
|
||||
href="/admin/login"
|
||||
className="text-mars-600 hover:text-mars-800 text-sm transition-colors"
|
||||
className="text-mars-600 hover:text-mars-800 text-xs transition-colors"
|
||||
>
|
||||
管理员登录
|
||||
</a>
|
||||
@@ -216,11 +218,11 @@ const HomePage = () => {
|
||||
</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-4 flex flex-col md:flex-row justify-between items-center text-gray-400 text-xs">
|
||||
<div className="mb-2 md:mb-0">
|
||||
© {new Date().getFullYear()} Boonlive OA System. All Rights Reserved.
|
||||
</div>
|
||||
<img src={纯字母LOGO} alt="纯字母LOGO" style={{ width: '136px', height: '51px' }} />
|
||||
<img src={纯字母LOGO} alt="纯字母LOGO" style={{ width: '100px', height: '38px' }} />
|
||||
</Footer>
|
||||
</Layout>
|
||||
);
|
||||
|
||||
@@ -29,6 +29,7 @@ interface LocationState {
|
||||
timeLimit?: number;
|
||||
subjectId?: string;
|
||||
taskId?: string;
|
||||
taskName?: string;
|
||||
}
|
||||
|
||||
const QuizPage = () => {
|
||||
@@ -43,6 +44,7 @@ const QuizPage = () => {
|
||||
const [timeLimit, setTimeLimit] = useState<number | null>(null);
|
||||
const [subjectId, setSubjectId] = useState<string>('');
|
||||
const [taskId, setTaskId] = useState<string>('');
|
||||
const [taskName, setTaskName] = useState<string>('');
|
||||
const lastTickSavedAtMsRef = useRef<number>(0);
|
||||
const saveDebounceTimerRef = useRef<number | null>(null);
|
||||
const [navDirection, setNavDirection] = useState<'next' | 'prev'>('next');
|
||||
@@ -124,6 +126,7 @@ const QuizPage = () => {
|
||||
if (state?.questions) {
|
||||
const nextSubjectId = state.subjectId || '';
|
||||
const nextTaskId = state.taskId || '';
|
||||
const nextTaskName = state.taskName || '';
|
||||
const nextTimeLimit = state.timeLimit || 60;
|
||||
|
||||
setQuestions(state.questions);
|
||||
@@ -133,6 +136,7 @@ const QuizPage = () => {
|
||||
setTimeLeft(nextTimeLimit * 60);
|
||||
setSubjectId(nextSubjectId);
|
||||
setTaskId(nextTaskId);
|
||||
setTaskName(nextTaskName);
|
||||
|
||||
const progressKey = buildProgressKey(user.id, nextSubjectId, nextTaskId);
|
||||
setActiveProgress(user.id, progressKey);
|
||||
@@ -308,6 +312,14 @@ const QuizPage = () => {
|
||||
setCurrentQuestionIndex(index);
|
||||
};
|
||||
|
||||
const handleJumpToFirstUnanswered = () => {
|
||||
const firstUnansweredIndex = questions.findIndex(q => !answers[q.id]);
|
||||
if (firstUnansweredIndex !== -1) {
|
||||
handleJumpTo(firstUnansweredIndex);
|
||||
}
|
||||
setAnswerSheetOpen(false);
|
||||
};
|
||||
|
||||
const handleSubmit = async (forceSubmit = false) => {
|
||||
try {
|
||||
setSubmitting(true);
|
||||
@@ -442,6 +454,7 @@ const QuizPage = () => {
|
||||
total={questions.length}
|
||||
timeLeft={timeLeft}
|
||||
onGiveUp={handleGiveUp}
|
||||
taskName={taskName}
|
||||
/>
|
||||
|
||||
<QuizProgress
|
||||
@@ -464,13 +477,13 @@ const QuizPage = () => {
|
||||
}
|
||||
`}
|
||||
>
|
||||
<div className="bg-white rounded-xl shadow-sm border border-gray-100 p-4 min-h-[350px]">
|
||||
<div className="mb-4">
|
||||
<span className={`inline-block px-2 py-0.5 rounded-md text-xs font-medium border ${getTagColor(currentQuestion.type)}`}>
|
||||
<div className="bg-white rounded-xl shadow-sm border border-gray-100 p-2 min-h-[280px]">
|
||||
<div className="mb-2">
|
||||
<span className={`inline-block px-1 py-0.5 rounded-md text-xs font-medium border ${getTagColor(currentQuestion.type)}`}>
|
||||
{questionTypeMap[currentQuestion.type]}
|
||||
</span>
|
||||
{currentQuestion.category && (
|
||||
<span className="ml-2 inline-block px-1.5 py-0.5 bg-gray-50 text-gray-600 text-xs rounded border border-gray-100">
|
||||
<span className="ml-2 inline-block px-1 py-0.5 bg-gray-50 text-gray-600 text-xs rounded border border-gray-100">
|
||||
{currentQuestion.category}
|
||||
</span>
|
||||
)}
|
||||
@@ -479,7 +492,7 @@ const QuizPage = () => {
|
||||
<h2
|
||||
ref={questionHeadingRef}
|
||||
tabIndex={-1}
|
||||
className="text-base font-medium text-gray-900 leading-relaxed mb-6 outline-none"
|
||||
className="text-sm font-medium text-gray-900 leading-tight mb-3 outline-none"
|
||||
>
|
||||
{currentQuestion.content}
|
||||
</h2>
|
||||
@@ -520,7 +533,7 @@ const QuizPage = () => {
|
||||
</div>
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={() => setAnswerSheetOpen(false)}
|
||||
onClick={handleJumpToFirstUnanswered}
|
||||
className="bg-[#00897B] hover:bg-[#00796B] text-xs h-7 px-3"
|
||||
>
|
||||
回到当前题
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Card, Button, Typography, Tag, Space, Spin, message } from 'antd';
|
||||
import { ClockCircleOutlined, BookOutlined, UserOutlined } from '@ant-design/icons';
|
||||
import { useNavigate, useLocation } from 'react-router-dom';
|
||||
import { Button, Typography, Tag, Space, Spin, message, Modal } from 'antd';
|
||||
import { ClockCircleOutlined, BookOutlined, RightOutlined } from '@ant-design/icons';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { useUser } from '../contexts';
|
||||
import { examSubjectAPI, examTaskAPI, quizAPI } from '../services/api';
|
||||
import { UserLayout } from '../layouts/UserLayout';
|
||||
@@ -34,10 +34,7 @@ export const SubjectSelectionPage: React.FC = () => {
|
||||
const [subjects, setSubjects] = useState<ExamSubject[]>([]);
|
||||
const [tasks, setTasks] = useState<ExamTask[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [selectedSubject, setSelectedSubject] = useState<string>('');
|
||||
const [selectedTask, setSelectedTask] = useState<string>('');
|
||||
const navigate = useNavigate();
|
||||
const location = useLocation();
|
||||
const { user } = useUser();
|
||||
|
||||
useEffect(() => {
|
||||
@@ -48,13 +45,6 @@ export const SubjectSelectionPage: React.FC = () => {
|
||||
}
|
||||
|
||||
fetchData();
|
||||
|
||||
// 如果从任务页面跳转过来,自动选择对应的任务
|
||||
const state = location.state as { selectedTask?: string };
|
||||
if (state?.selectedTask) {
|
||||
setSelectedTask(state.selectedTask);
|
||||
setSelectedSubject('');
|
||||
}
|
||||
}, [user?.id, navigate]);
|
||||
|
||||
const fetchData = async () => {
|
||||
@@ -75,37 +65,65 @@ export const SubjectSelectionPage: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const startQuiz = async () => {
|
||||
if (!selectedSubject && !selectedTask) {
|
||||
message.warning('请选择考试科目或考试任务');
|
||||
const getTaskStatus = (task: ExamTask) => {
|
||||
const now = new Date();
|
||||
const startAt = new Date(task.startAt);
|
||||
const endAt = new Date(task.endAt);
|
||||
const usedAttempts = Number(task.usedAttempts) || 0;
|
||||
const maxAttempts = Number(task.maxAttempts) || 3;
|
||||
|
||||
if (now < startAt) {
|
||||
return 'notStarted';
|
||||
} else if (now >= startAt && now <= endAt && usedAttempts < maxAttempts) {
|
||||
return 'ongoing';
|
||||
} else {
|
||||
return 'completed';
|
||||
}
|
||||
};
|
||||
|
||||
const getTasksByStatus = (status: 'ongoing' | 'completed' | 'notStarted') => {
|
||||
return tasks.filter(task => getTaskStatus(task) === status);
|
||||
};
|
||||
|
||||
const startQuiz = async (taskId: string) => {
|
||||
if (!taskId) {
|
||||
message.warning('请选择考试任务');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
if (selectedTask) {
|
||||
const task = tasks.find((t) => t.id === selectedTask);
|
||||
const usedAttempts = Number(task?.usedAttempts) || 0;
|
||||
const maxAttempts = Number(task?.maxAttempts) || 3;
|
||||
if (usedAttempts >= maxAttempts) {
|
||||
message.error('考试次数已用尽');
|
||||
return;
|
||||
const task = tasks.find((t) => t.id === taskId);
|
||||
const usedAttempts = Number(task?.usedAttempts) || 0;
|
||||
const maxAttempts = Number(task?.maxAttempts) || 3;
|
||||
const remainingAttempts = maxAttempts - usedAttempts;
|
||||
|
||||
if (usedAttempts >= maxAttempts) {
|
||||
message.error('考试次数已用尽');
|
||||
return;
|
||||
}
|
||||
|
||||
Modal.confirm({
|
||||
title: '确认开始考试',
|
||||
content: `是否现在开始考试,你还有${remainingAttempts}次重试机会`,
|
||||
okText: '确认',
|
||||
cancelText: '取消',
|
||||
onOk: async () => {
|
||||
try {
|
||||
const response = await quizAPI.generateQuiz(user?.id || '', undefined, taskId) as any;
|
||||
const { questions, totalScore, timeLimit } = response.data;
|
||||
navigate('/quiz', {
|
||||
state: {
|
||||
questions,
|
||||
totalScore,
|
||||
timeLimit,
|
||||
taskId: taskId,
|
||||
taskName: task?.name || ''
|
||||
}
|
||||
});
|
||||
} catch (error: any) {
|
||||
message.error(error.message || '生成试卷失败');
|
||||
}
|
||||
}
|
||||
|
||||
const response = await quizAPI.generateQuiz(user?.id || '', selectedSubject || undefined, selectedTask || undefined) as any;
|
||||
const { questions, totalScore, timeLimit } = response.data;
|
||||
navigate('/quiz', {
|
||||
state: {
|
||||
questions,
|
||||
totalScore,
|
||||
timeLimit,
|
||||
subjectId: selectedSubject,
|
||||
taskId: selectedTask
|
||||
}
|
||||
});
|
||||
} catch (error: any) {
|
||||
message.error(error.message || '生成试卷失败');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
const formatTypeRatio = (typeRatios: Record<string, number>) => {
|
||||
@@ -137,169 +155,248 @@ export const SubjectSelectionPage: React.FC = () => {
|
||||
return (
|
||||
<UserLayout>
|
||||
<div className="container mx-auto px-4 max-w-md">
|
||||
<div className="mb-4 text-center">
|
||||
<Title level={3} className="!text-mars-600 mb-1 !text-lg">
|
||||
选择考试科目
|
||||
</Title>
|
||||
<Text type="secondary" className="block text-sm">
|
||||
请选择您要参加的考试科目或考试任务
|
||||
</Text>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 gap-4">
|
||||
{/* 考试科目选择 */}
|
||||
<div>
|
||||
<div className="flex items-center mb-3 border-b border-gray-200 pb-1">
|
||||
<BookOutlined className="text-lg mr-2 text-mars-600" />
|
||||
<Title level={4} className="!mb-0 !text-gray-700 !text-base">考试科目</Title>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
{subjects.map((subject) => (
|
||||
<Card
|
||||
key={subject.id}
|
||||
className={`cursor-pointer transition-all duration-300 border-l-4 ${
|
||||
selectedSubject === subject.id
|
||||
? 'border-l-mars-500 border-t border-r border-b border-mars-200 shadow-md bg-mars-50'
|
||||
: 'border-l-transparent hover:border-l-mars-300 hover:shadow-md'
|
||||
}`}
|
||||
onClick={() => {
|
||||
setSelectedSubject(subject.id);
|
||||
setSelectedTask('');
|
||||
}}
|
||||
>
|
||||
<div className="flex justify-between items-start">
|
||||
<div className="flex-1">
|
||||
<Title level={5} className={`mb-1 ${selectedSubject === subject.id ? 'text-mars-700' : 'text-gray-800'} !text-sm`}>
|
||||
{subject.name}
|
||||
</Title>
|
||||
<Space direction="vertical" size="small" className="mb-2">
|
||||
<div className="flex items-center">
|
||||
<ClockCircleOutlined className="mr-1 text-gray-400 text-xs" />
|
||||
<Text className="text-gray-600 text-xs">{subject.timeLimitMinutes}分钟</Text>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<span className="mr-1 text-gray-400 text-xs">总分:</span>
|
||||
<Text strong className="text-gray-700 text-xs">{subject.totalScore}分</Text>
|
||||
</div>
|
||||
</Space>
|
||||
<div className="text-xs text-gray-500 bg-gray-50 p-1.5 rounded">
|
||||
<div className="mb-0.5 font-medium text-xs">题型分布:</div>
|
||||
<div className="text-xs">{formatTypeRatio(subject.typeRatios)}</div>
|
||||
</div>
|
||||
</div>
|
||||
{selectedSubject === subject.id && (
|
||||
<div className="text-mars-600">
|
||||
<div className="w-6 h-6 bg-mars-500 rounded-full flex items-center justify-center shadow-sm">
|
||||
<span className="text-white text-sm font-bold">✓</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 考试任务选择 */}
|
||||
|
||||
{/* 考试任务选择 - 按状态分组 */}
|
||||
<div>
|
||||
<div className="flex items-center mb-3 border-b border-gray-200 pb-1">
|
||||
<UserOutlined className="text-lg mr-2 text-mars-400" />
|
||||
<Title level={4} className="!mb-0 !text-gray-700 !text-base">考试任务</Title>
|
||||
<BookOutlined className="text-lg mr-2 text-mars-400" />
|
||||
<Title level={4} className="!mb-0 !text-gray-700 !text-base">我的考试任务</Title>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
{tasks.map((task) => {
|
||||
const subject = subjects.find(s => s.id === task.subjectId);
|
||||
const usedAttempts = Number(task.usedAttempts) || 0;
|
||||
const maxAttempts = Number(task.maxAttempts) || 3;
|
||||
const attemptsExhausted = usedAttempts >= maxAttempts;
|
||||
return (
|
||||
<Card
|
||||
key={task.id}
|
||||
className={`cursor-pointer transition-all duration-300 border-l-4 ${
|
||||
selectedTask === task.id
|
||||
? 'border-l-mars-400 border-t border-r border-b border-mars-200 shadow-md bg-mars-50'
|
||||
: 'border-l-transparent hover:border-l-mars-300 hover:shadow-md'
|
||||
}`}
|
||||
onClick={() => {
|
||||
if (attemptsExhausted) return;
|
||||
setSelectedTask(task.id);
|
||||
setSelectedSubject('');
|
||||
}}
|
||||
>
|
||||
<div className="flex justify-between items-start">
|
||||
<div className="flex-1">
|
||||
<Title level={5} className={`mb-1 ${selectedTask === task.id ? 'text-mars-700' : 'text-gray-800'} !text-sm`}>
|
||||
{task.name}
|
||||
</Title>
|
||||
<div className="mb-2">
|
||||
<Tag color={attemptsExhausted ? 'red' : 'blue'} className="text-xs">
|
||||
{usedAttempts}/{maxAttempts}
|
||||
</Tag>
|
||||
{typeof task.bestScore === 'number' ? (
|
||||
<Tag color="green" className="text-xs">最高分 {task.bestScore} 分</Tag>
|
||||
) : null}
|
||||
{attemptsExhausted ? <Tag color="red" className="text-xs">次数用尽</Tag> : null}
|
||||
</div>
|
||||
<Space direction="vertical" size="small" className="mb-2">
|
||||
<div className="flex items-center">
|
||||
<BookOutlined className="mr-1 text-gray-400 text-xs" />
|
||||
<Text className="text-gray-600 text-xs">{subject?.name || '未知科目'}</Text>
|
||||
</div>
|
||||
<div className="flex items-center">
|
||||
<ClockCircleOutlined className="mr-1 text-gray-400 text-xs" />
|
||||
<Text className="text-gray-600 text-xs">
|
||||
{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 className="space-y-4">
|
||||
{/* 进行中 */}
|
||||
<div>
|
||||
<div className="flex items-center mb-2">
|
||||
<span className="inline-block w-2 h-2 bg-green-500 rounded-full mr-2"></span>
|
||||
<Text className="text-sm font-medium text-gray-700">进行中</Text>
|
||||
<Tag color="green" className="ml-2 text-xs">{getTasksByStatus('ongoing').length}</Tag>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{getTasksByStatus('ongoing').map((task) => {
|
||||
const subject = subjects.find(s => s.id === task.subjectId);
|
||||
const usedAttempts = Number(task.usedAttempts) || 0;
|
||||
const maxAttempts = Number(task.maxAttempts) || 3;
|
||||
return (
|
||||
<Button
|
||||
key={task.id}
|
||||
type="default"
|
||||
block
|
||||
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-1">
|
||||
<div className="flex justify-between 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>
|
||||
</div>
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<div className="flex items-center">
|
||||
<BookOutlined className="mr-1 text-gray-400 text-xs" />
|
||||
<Text className="text-gray-600 text-xs">{subject?.name || '未知科目'}</Text>
|
||||
</div>
|
||||
<div>
|
||||
<Tag color="green" className="text-xs">
|
||||
{usedAttempts}/{maxAttempts}
|
||||
</Tag>
|
||||
{typeof task.bestScore === 'number' ? (
|
||||
<Tag color="green" className="text-xs">最高分 {task.bestScore} 分</Tag>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<div className="flex items-center">
|
||||
<ClockCircleOutlined className="mr-1 text-gray-400 text-xs" />
|
||||
<Text className="text-gray-600 text-xs">
|
||||
{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>
|
||||
)}
|
||||
</Space>
|
||||
</div>
|
||||
{selectedTask === task.id && (
|
||||
<div className="text-mars-400">
|
||||
<div className="w-6 h-6 bg-mars-400 rounded-full flex items-center justify-center shadow-sm">
|
||||
<span className="text-white text-sm font-bold">✓</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
{getTasksByStatus('ongoing').length === 0 && (
|
||||
<div className="text-center py-4 bg-gray-50 border-dashed border-2 border-gray-200 rounded">
|
||||
<Text type="secondary" className="text-sm">暂无进行中的任务</Text>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
})}
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 已完成 */}
|
||||
<div>
|
||||
<div className="flex items-center mb-2">
|
||||
<span className="inline-block w-2 h-2 bg-gray-400 rounded-full mr-2"></span>
|
||||
<Text className="text-sm font-medium text-gray-700">已完成</Text>
|
||||
<Tag color="default" className="ml-2 text-xs">{getTasksByStatus('completed').length}</Tag>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{getTasksByStatus('completed').map((task) => {
|
||||
const subject = subjects.find(s => s.id === task.subjectId);
|
||||
const usedAttempts = Number(task.usedAttempts) || 0;
|
||||
const maxAttempts = Number(task.maxAttempts) || 3;
|
||||
const attemptsExhausted = usedAttempts >= maxAttempts;
|
||||
return (
|
||||
<Button
|
||||
key={task.id}
|
||||
type="default"
|
||||
block
|
||||
disabled={attemptsExhausted}
|
||||
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-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>
|
||||
</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" />
|
||||
<Text className="text-gray-600 text-xs">{subject?.name || '未知科目'}</Text>
|
||||
</div>
|
||||
<div>
|
||||
<Tag color={attemptsExhausted ? 'red' : 'default'} className="text-xs">
|
||||
{usedAttempts}/{maxAttempts}
|
||||
</Tag>
|
||||
{typeof task.bestScore === 'number' ? (
|
||||
<Tag color="green" className="text-xs">最高分 {task.bestScore} 分</Tag>
|
||||
) : null}
|
||||
{attemptsExhausted ? <Tag color="red" className="text-xs">次数用尽</Tag> : null}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<div className="flex items-center">
|
||||
<ClockCircleOutlined className="mr-1 text-gray-400 text-xs" />
|
||||
<Text className="text-gray-600 text-xs">
|
||||
{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>
|
||||
</div>
|
||||
</div>
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
{getTasksByStatus('completed').length === 0 && (
|
||||
<div className="text-center py-4 bg-gray-50 border-dashed border-2 border-gray-200 rounded">
|
||||
<Text type="secondary" className="text-sm">暂无已完成的任务</Text>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 未开始 */}
|
||||
<div>
|
||||
<div className="flex items-center mb-2">
|
||||
<span className="inline-block w-2 h-2 bg-blue-400 rounded-full mr-2"></span>
|
||||
<Text className="text-sm font-medium text-gray-700">未开始</Text>
|
||||
<Tag color="blue" className="ml-2 text-xs">{getTasksByStatus('notStarted').length}</Tag>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{getTasksByStatus('notStarted').map((task) => {
|
||||
const subject = subjects.find(s => s.id === task.subjectId);
|
||||
const usedAttempts = Number(task.usedAttempts) || 0;
|
||||
const maxAttempts = Number(task.maxAttempts) || 3;
|
||||
return (
|
||||
<Button
|
||||
key={task.id}
|
||||
type="default"
|
||||
block
|
||||
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-1">
|
||||
<Title level={5} className="mb-1 text-gray-800 !text-sm">
|
||||
{task.name}
|
||||
</Title>
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<div className="flex items-center">
|
||||
<BookOutlined className="mr-1 text-gray-400 text-xs" />
|
||||
<Text className="text-gray-600 text-xs">{subject?.name || '未知科目'}</Text>
|
||||
</div>
|
||||
<div>
|
||||
<Tag color="blue" className="text-xs">
|
||||
{usedAttempts}/{maxAttempts}
|
||||
</Tag>
|
||||
{typeof task.bestScore === 'number' ? (
|
||||
<Tag color="green" className="text-xs">最高分 {task.bestScore} 分</Tag>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<div className="flex items-center">
|
||||
<ClockCircleOutlined className="mr-1 text-gray-400 text-xs" />
|
||||
<Text className="text-gray-600 text-xs">
|
||||
{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>
|
||||
</div>
|
||||
</div>
|
||||
</Button>
|
||||
);
|
||||
})}
|
||||
{getTasksByStatus('notStarted').length === 0 && (
|
||||
<div className="text-center py-4 bg-gray-50 border-dashed border-2 border-gray-200 rounded">
|
||||
<Text type="secondary" className="text-sm">暂无未开始的任务</Text>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{tasks.length === 0 && (
|
||||
<Card className="text-center py-8 bg-gray-50 border-dashed border-2 border-gray-200">
|
||||
<div className="text-center py-8 bg-gray-50 border-dashed border-2 border-gray-200 rounded">
|
||||
<Text type="secondary" className="text-sm">暂无可用考试任务</Text>
|
||||
</Card>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 text-center space-x-3">
|
||||
<Button
|
||||
type="primary"
|
||||
size="large"
|
||||
className="px-6 h-10 text-sm font-medium shadow-lg hover:scale-105 transition-transform bg-mars-500 hover:bg-mars-600 border-none"
|
||||
onClick={startQuiz}
|
||||
disabled={!selectedSubject && !selectedTask}
|
||||
>
|
||||
开始考试
|
||||
</Button>
|
||||
<div className="mt-6 text-center">
|
||||
<Button
|
||||
type="default"
|
||||
size="large"
|
||||
className="px-4 h-10 text-sm hover:border-mars-500 hover:text-mars-500"
|
||||
onClick={() => navigate('/tasks')}
|
||||
icon={<UserOutlined />}
|
||||
>
|
||||
查看我的任务
|
||||
查看我的任务列表
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -91,16 +91,18 @@ export const UserTaskPage: React.FC = () => {
|
||||
title: '任务名称',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
render: (text: string) => <Text strong>{text}</Text>
|
||||
width: 100,
|
||||
render: (text: string) => <Text strong style={{ fontSize: '12px' }}>{text}</Text>
|
||||
},
|
||||
{
|
||||
title: '考试科目',
|
||||
dataIndex: 'subjectName',
|
||||
key: 'subjectName',
|
||||
width: 90,
|
||||
render: (text: string) => (
|
||||
<Space>
|
||||
<BookOutlined className="text-mars-600" />
|
||||
<Text>{text}</Text>
|
||||
<Space size={4}>
|
||||
<BookOutlined style={{ fontSize: '12px' }} className="text-mars-600" />
|
||||
<Text style={{ fontSize: '12px' }}>{text}</Text>
|
||||
</Space>
|
||||
)
|
||||
},
|
||||
@@ -108,33 +110,36 @@ export const UserTaskPage: React.FC = () => {
|
||||
title: '总分',
|
||||
dataIndex: 'totalScore',
|
||||
key: 'totalScore',
|
||||
render: (score: number) => <Text strong>{score}分</Text>
|
||||
width: 60,
|
||||
render: (score: number) => <Text strong style={{ fontSize: '12px' }}>{score}分</Text>
|
||||
},
|
||||
{
|
||||
title: '时长',
|
||||
dataIndex: 'timeLimitMinutes',
|
||||
key: 'timeLimitMinutes',
|
||||
width: 70,
|
||||
render: (minutes: number) => (
|
||||
<Space>
|
||||
<ClockCircleOutlined className="text-gray-500" />
|
||||
<Text>{minutes}分钟</Text>
|
||||
<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>
|
||||
<CalendarOutlined className="text-gray-500" />
|
||||
<Text type="secondary" style={{ fontSize: '12px' }}>
|
||||
<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>
|
||||
<CalendarOutlined className="text-gray-500" />
|
||||
<Text type="secondary" style={{ fontSize: '12px' }}>
|
||||
<Space size={4}>
|
||||
<CalendarOutlined style={{ fontSize: '11px' }} className="text-gray-500" />
|
||||
<Text type="secondary" style={{ fontSize: '11px' }}>
|
||||
{new Date(record.endAt).toLocaleDateString()}
|
||||
</Text>
|
||||
</Space>
|
||||
@@ -144,8 +149,9 @@ export const UserTaskPage: React.FC = () => {
|
||||
{
|
||||
title: '状态',
|
||||
key: 'status',
|
||||
width: 70,
|
||||
render: (record: ExamTask) => (
|
||||
<Tag color={getStatusColor(record)} className="rounded-full px-3">
|
||||
<Tag color={getStatusColor(record)} className="rounded-full px-2" style={{ fontSize: '11px' }}>
|
||||
{getStatusText(record)}
|
||||
</Tag>
|
||||
)
|
||||
@@ -153,8 +159,9 @@ export const UserTaskPage: React.FC = () => {
|
||||
{
|
||||
title: '次数',
|
||||
key: 'attempts',
|
||||
width: 50,
|
||||
render: (record: ExamTask) => (
|
||||
<Text>
|
||||
<Text style={{ fontSize: '12px' }}>
|
||||
{record.usedAttempts}/{record.maxAttempts}
|
||||
</Text>
|
||||
),
|
||||
@@ -163,32 +170,8 @@ export const UserTaskPage: React.FC = () => {
|
||||
title: '最高分',
|
||||
dataIndex: 'bestScore',
|
||||
key: 'bestScore',
|
||||
render: (score: number) => <Text strong>{score}分</Text>,
|
||||
},
|
||||
{
|
||||
title: <div style={{ textAlign: 'center' }}>操作</div>,
|
||||
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 && record.usedAttempts < record.maxAttempts;
|
||||
|
||||
return (
|
||||
<Space>
|
||||
<Button
|
||||
type="primary"
|
||||
size="small"
|
||||
onClick={() => startTask(record)}
|
||||
disabled={!canStart}
|
||||
icon={<CheckCircleOutlined />}
|
||||
className={canStart ? "bg-mars-500 hover:bg-mars-600 border-none" : ""}
|
||||
>
|
||||
{canStart ? '开始考试' : record.usedAttempts >= record.maxAttempts ? '次数用尽' : '不可用'}
|
||||
</Button>
|
||||
</Space>
|
||||
);
|
||||
}
|
||||
width: 60,
|
||||
render: (score: number) => <Text strong style={{ fontSize: '12px' }}>{score}分</Text>,
|
||||
}
|
||||
];
|
||||
|
||||
@@ -204,35 +187,32 @@ export const UserTaskPage: React.FC = () => {
|
||||
|
||||
return (
|
||||
<UserLayout>
|
||||
<div className="container mx-auto px-4 max-w-md">
|
||||
<div className="mb-4 text-center">
|
||||
<Title level={3} className="!text-mars-600 mb-1 !text-lg">
|
||||
我的考试任务
|
||||
</Title>
|
||||
<Text type="secondary" className="block text-sm">
|
||||
查看您被分派的所有考试任务
|
||||
</Text>
|
||||
</div>
|
||||
<div className="container mx-auto px-2 max-w-md">
|
||||
|
||||
<Card className="shadow-md border-t-4 border-t-mars-500 rounded-xl">
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={tasks}
|
||||
rowKey="id"
|
||||
pagination={{
|
||||
pageSize: 5,
|
||||
showSizeChanger: false,
|
||||
showTotal: (total) => `共 ${total} 条`
|
||||
}}
|
||||
locale={{
|
||||
emptyText: '暂无考试任务'
|
||||
}}
|
||||
size="small"
|
||||
scroll={{ x: 'max-content' }}
|
||||
/>
|
||||
</Card>
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={tasks}
|
||||
rowKey="id"
|
||||
size="small"
|
||||
pagination={{
|
||||
pageSize: 5,
|
||||
showSizeChanger: false,
|
||||
showTotal: (total) => `共 ${total} 条`,
|
||||
size: 'small'
|
||||
}}
|
||||
locale={{
|
||||
emptyText: '暂无考试任务'
|
||||
}}
|
||||
size="small"
|
||||
scroll={{ x: 'max-content' }}
|
||||
className="mobile-table"
|
||||
style={{
|
||||
fontSize: '12px'
|
||||
}}
|
||||
rowClassName={() => 'mobile-table-row'}
|
||||
/>
|
||||
|
||||
<div className="mt-6 text-center">
|
||||
<div className="mt-4 text-center">
|
||||
<Button
|
||||
type="default"
|
||||
size="large"
|
||||
@@ -240,7 +220,7 @@ export const UserTaskPage: React.FC = () => {
|
||||
icon={<BookOutlined />}
|
||||
className="px-6 h-10 text-sm hover:border-mars-500 hover:text-mars-500"
|
||||
>
|
||||
返回科目选择
|
||||
返回任务选择
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -460,6 +460,7 @@ const AdminDashboardPage = () => {
|
||||
total: taskStatsPagination.total,
|
||||
showSizeChanger: false,
|
||||
onChange: (page) => fetchTaskStats(page),
|
||||
size: 'small',
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
@@ -95,16 +95,16 @@ const AdminLoginPage = () => {
|
||||
|
||||
return (
|
||||
<Layout className="min-h-screen bg-gray-50">
|
||||
<Header className="bg-white shadow-sm flex items-center px-6 h-16 sticky top-0 z-10">
|
||||
<img src={主要LOGO} alt="主要LOGO" style={{ width: '180px', height: '72px' }} />
|
||||
<Header className="bg-white shadow-sm flex items-center px-4 h-12 sticky top-0 z-10">
|
||||
<img src={主要LOGO} alt="主要LOGO" style={{ width: '120px', height: '48px' }} />
|
||||
</Header>
|
||||
|
||||
<Content className="flex items-center justify-center p-4 bg-gradient-to-br from-mars-50 to-white">
|
||||
<Content className="flex items-center justify-center p-2 bg-gradient-to-br from-mars-50 to-white">
|
||||
<div className="w-full max-w-md">
|
||||
<Card className="shadow-xl border-t-4 border-t-mars-500">
|
||||
<div className="text-center mb-6">
|
||||
<Card className="shadow-xl border-t-4 border-t-mars-500 p-2">
|
||||
<div className="text-center mb-3">
|
||||
<h1 className="text-2xl font-bold text-mars-600 mb-2">管理员登录</h1>
|
||||
<p className="text-gray-600">请输入管理员账号密码</p>
|
||||
<p className="text-gray-600 text-xs">请输入管理员账号密码</p>
|
||||
</div>
|
||||
|
||||
<Form
|
||||
@@ -112,11 +112,10 @@ const AdminLoginPage = () => {
|
||||
onFinish={handleSubmit}
|
||||
autoComplete="off"
|
||||
form={form}
|
||||
size="large"
|
||||
>
|
||||
{/* 最近登录记录下拉选择 */}
|
||||
{loginRecords.length > 0 && (
|
||||
<Form.Item label="最近登录" className="mb-4">
|
||||
<Form.Item label="最近登录" className="mb-2">
|
||||
<Select
|
||||
placeholder="选择最近登录记录"
|
||||
className="w-full"
|
||||
@@ -131,7 +130,7 @@ const AdminLoginPage = () => {
|
||||
value: `${record.username}-${record.timestamp}`,
|
||||
label: (
|
||||
<div className="flex flex-col p-1" style={{ minHeight: '40px', justifyContent: 'center' }}>
|
||||
<span className="font-medium block">{record.username}</span>
|
||||
<span className="font-medium block text-xs">{record.username}</span>
|
||||
<span className="text-xs text-gray-500 block">
|
||||
{new Date(record.timestamp).toLocaleString()}
|
||||
</span>
|
||||
@@ -149,7 +148,7 @@ const AdminLoginPage = () => {
|
||||
{ required: true, message: '请输入用户名' },
|
||||
{ min: 3, message: '用户名至少3个字符' }
|
||||
]}
|
||||
className="mb-4"
|
||||
className="mb-2"
|
||||
>
|
||||
<Input
|
||||
placeholder="请输入用户名"
|
||||
@@ -164,7 +163,7 @@ const AdminLoginPage = () => {
|
||||
{ required: true, message: '请输入密码' },
|
||||
{ min: 6, message: '密码至少6个字符' }
|
||||
]}
|
||||
className="mb-4"
|
||||
className="mb-2"
|
||||
>
|
||||
<Input.Password
|
||||
placeholder="请输入密码"
|
||||
@@ -173,23 +172,23 @@ const AdminLoginPage = () => {
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item className="mb-0 mt-8">
|
||||
<Form.Item className="mb-0 mt-3">
|
||||
<Button
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
loading={loading}
|
||||
block
|
||||
className="bg-mars-500 hover:!bg-mars-600 border-none shadow-md h-12 text-lg font-medium"
|
||||
className="bg-mars-500 hover:!bg-mars-600 border-none shadow-md h-9 text-sm font-medium"
|
||||
>
|
||||
登录
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
|
||||
<div className="mt-6 text-center">
|
||||
<div className="mt-3 text-center">
|
||||
<a
|
||||
href="/"
|
||||
className="text-mars-600 hover:text-mars-800 text-sm transition-colors"
|
||||
className="text-mars-600 hover:text-mars-800 text-xs transition-colors"
|
||||
>
|
||||
返回用户端
|
||||
</a>
|
||||
@@ -198,11 +197,11 @@ const AdminLoginPage = () => {
|
||||
</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-4 flex flex-col md:flex-row justify-between items-center text-gray-400 text-xs">
|
||||
<div className="mb-2 md:mb-0">
|
||||
© {new Date().getFullYear()} Boonlive OA System. All Rights Reserved.
|
||||
</div>
|
||||
<img src={纯字母LOGO} alt="纯字母LOGO" style={{ width: '136px', height: '51px' }} />
|
||||
<img src={纯字母LOGO} alt="纯字母LOGO" style={{ width: '100px', height: '38px' }} />
|
||||
</Footer>
|
||||
</Layout>
|
||||
);
|
||||
|
||||
@@ -476,11 +476,13 @@ const ExamSubjectPage = () => {
|
||||
columns={columns}
|
||||
dataSource={subjects}
|
||||
rowKey="id"
|
||||
size="small"
|
||||
loading={loading}
|
||||
pagination={{
|
||||
pageSize: 10,
|
||||
showSizeChanger: true,
|
||||
showTotal: (total) => `共 ${total} 条`,
|
||||
size: 'small',
|
||||
}}
|
||||
/>
|
||||
|
||||
|
||||
@@ -213,32 +213,35 @@ const ExamTaskPage = () => {
|
||||
title: '任务名称',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
width: 250,
|
||||
width: 210,
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: '考试科目',
|
||||
dataIndex: 'subjectName',
|
||||
key: 'subjectName',
|
||||
width: 200,
|
||||
width: 180,
|
||||
ellipsis: true,
|
||||
},
|
||||
{
|
||||
title: '开始时间',
|
||||
dataIndex: 'startAt',
|
||||
key: 'startAt',
|
||||
width: 110,
|
||||
render: (text: string) => dayjs(text).format('YYYY-MM-DD HH:mm'),
|
||||
},
|
||||
{
|
||||
title: '结束时间',
|
||||
dataIndex: 'endAt',
|
||||
key: 'endAt',
|
||||
width: 110,
|
||||
render: (text: string) => dayjs(text).format('YYYY-MM-DD HH:mm'),
|
||||
},
|
||||
{
|
||||
title: '考试进程',
|
||||
dataIndex: ['startAt', 'endAt'],
|
||||
key: 'progress',
|
||||
width: 160,
|
||||
render: (_: any, record: ExamTask) => {
|
||||
const now = dayjs();
|
||||
const start = dayjs(record.startAt);
|
||||
@@ -246,13 +249,10 @@ const ExamTaskPage = () => {
|
||||
|
||||
let progress = 0;
|
||||
if (now < start) {
|
||||
// 尚未开始
|
||||
progress = 0;
|
||||
} else if (now > end) {
|
||||
// 已结束
|
||||
progress = 100;
|
||||
} else {
|
||||
// 进行中
|
||||
const totalDuration = end.diff(start, 'millisecond');
|
||||
const elapsedDuration = now.diff(start, 'millisecond');
|
||||
progress = Math.round((elapsedDuration / totalDuration) * 100);
|
||||
@@ -277,40 +277,46 @@ const ExamTaskPage = () => {
|
||||
title: '参与人数',
|
||||
dataIndex: 'userCount',
|
||||
key: 'userCount',
|
||||
width: 75,
|
||||
render: (count: number) => `${count} 人`,
|
||||
},
|
||||
{
|
||||
title: '已完成人数',
|
||||
dataIndex: 'completedUsers',
|
||||
key: 'completedUsers',
|
||||
width: 75,
|
||||
render: (count: number) => `${count} 人`,
|
||||
},
|
||||
{
|
||||
title: '合格率',
|
||||
dataIndex: 'passRate',
|
||||
key: 'passRate',
|
||||
width: 75,
|
||||
render: (rate: number) => `${rate}%`,
|
||||
},
|
||||
{
|
||||
title: '优秀率',
|
||||
dataIndex: 'excellentRate',
|
||||
key: 'excellentRate',
|
||||
width: 75,
|
||||
render: (rate: number) => `${rate}%`,
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
dataIndex: 'createdAt',
|
||||
key: 'createdAt',
|
||||
width: 110,
|
||||
render: (text: string) => dayjs(text).format('YYYY-MM-DD HH:mm'),
|
||||
},
|
||||
{
|
||||
title: <div style={{ textAlign: 'center' }}>操作</div>,
|
||||
key: 'action',
|
||||
width: 120,
|
||||
width: 230,
|
||||
render: (_: any, record: ExamTask) => (
|
||||
<Space direction="vertical" size={0}>
|
||||
<Space size="small">
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<EditOutlined />}
|
||||
onClick={() => handleEdit(record)}
|
||||
>
|
||||
@@ -320,6 +326,7 @@ const ExamTaskPage = () => {
|
||||
type="text"
|
||||
icon={<FileTextOutlined />}
|
||||
onClick={() => handleReport(record.id)}
|
||||
size="small"
|
||||
>
|
||||
报表
|
||||
</Button>
|
||||
@@ -329,7 +336,7 @@ const ExamTaskPage = () => {
|
||||
okText="确定"
|
||||
cancelText="取消"
|
||||
>
|
||||
<Button type="text" danger icon={<DeleteOutlined />}>
|
||||
<Button type="text" danger icon={<DeleteOutlined />} size="small">
|
||||
删除
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
@@ -351,11 +358,14 @@ const ExamTaskPage = () => {
|
||||
columns={columns}
|
||||
dataSource={tasks}
|
||||
rowKey="id"
|
||||
size="small"
|
||||
loading={loading}
|
||||
scroll={{ x: 1400 }}
|
||||
pagination={{
|
||||
pageSize: 10,
|
||||
showSizeChanger: true,
|
||||
showTotal: (total) => `共 ${total} 条`,
|
||||
size: 'small',
|
||||
}}
|
||||
/>
|
||||
|
||||
@@ -522,6 +532,7 @@ const ExamTaskPage = () => {
|
||||
]}
|
||||
dataSource={reportData.details}
|
||||
rowKey="userId"
|
||||
size="small"
|
||||
pagination={false}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -86,6 +86,8 @@ const QuestionCategoryPage = () => {
|
||||
title: '类别名称',
|
||||
dataIndex: 'name',
|
||||
key: 'name',
|
||||
align: 'center' as const,
|
||||
width: 250,
|
||||
render: (name: string) => (
|
||||
<div className="flex items-center">
|
||||
<Tag color={getCategoryColorHex(name)} className="mr-2">
|
||||
@@ -112,6 +114,8 @@ const QuestionCategoryPage = () => {
|
||||
},
|
||||
{
|
||||
title: '创建时间',
|
||||
align: 'center' as const,
|
||||
width: 200,
|
||||
dataIndex: 'createdAt',
|
||||
key: 'createdAt',
|
||||
render: (text: string) => new Date(text).toLocaleString(),
|
||||
@@ -119,10 +123,13 @@ const QuestionCategoryPage = () => {
|
||||
{
|
||||
title: <div style={{ textAlign: 'center' }}>操作</div>,
|
||||
key: 'action',
|
||||
align: 'center' as const,
|
||||
width: 200,
|
||||
render: (_: any, record: QuestionCategory) => (
|
||||
<Space>
|
||||
<Button
|
||||
type="text"
|
||||
size="small"
|
||||
icon={<EditOutlined />}
|
||||
onClick={() => handleEdit(record)}
|
||||
>
|
||||
@@ -134,7 +141,7 @@ const QuestionCategoryPage = () => {
|
||||
okText="确定"
|
||||
cancelText="取消"
|
||||
>
|
||||
<Button type="text" danger icon={<DeleteOutlined />}>
|
||||
<Button type="text" size="small" danger icon={<DeleteOutlined />}>
|
||||
删除
|
||||
</Button>
|
||||
</Popconfirm>
|
||||
@@ -161,6 +168,7 @@ const QuestionCategoryPage = () => {
|
||||
columns={columns}
|
||||
dataSource={categories}
|
||||
rowKey="id"
|
||||
size="small"
|
||||
loading={loading}
|
||||
pagination={{
|
||||
pageSize: 10,
|
||||
|
||||
@@ -65,7 +65,8 @@ const QuestionManagePage = () => {
|
||||
const [pagination, setPagination] = useState({
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
total: 0
|
||||
total: 0,
|
||||
size: 'small' as const,
|
||||
});
|
||||
// 动态选项
|
||||
const [availableTypes, setAvailableTypes] = useState<string[]>([]);
|
||||
@@ -543,21 +544,20 @@ const QuestionManagePage = () => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<Card className="shadow-sm">
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={questions}
|
||||
rowKey="id"
|
||||
loading={loading}
|
||||
pagination={{
|
||||
...pagination,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
showTotal: (total) => `共 ${total} 条`,
|
||||
}}
|
||||
onChange={(newPagination) => setPagination(newPagination as any)}
|
||||
/>
|
||||
</Card>
|
||||
<Table
|
||||
columns={columns}
|
||||
dataSource={questions}
|
||||
rowKey="id"
|
||||
size="small"
|
||||
loading={loading}
|
||||
pagination={{
|
||||
...pagination,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
showTotal: (total) => `共 ${total} 条`,
|
||||
}}
|
||||
onChange={(newPagination) => setPagination(newPagination as any)}
|
||||
/>
|
||||
|
||||
{/* 编辑/新增模态框 */}
|
||||
<Modal
|
||||
|
||||
@@ -16,7 +16,7 @@ const QuestionTextImportPage = () => {
|
||||
const [parseErrors, setParseErrors] = useState<string[]>([]);
|
||||
const [questions, setQuestions] = useState<ImportQuestion[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [tablePagination, setTablePagination] = useState({ current: 1, pageSize: 10 });
|
||||
const [tablePagination, setTablePagination] = useState({ current: 1, pageSize: 10, size: 'small' as const });
|
||||
|
||||
const exampleText = [
|
||||
'题型|题目类别|分值|题目内容|选项|答案|解析',
|
||||
@@ -229,6 +229,7 @@ const QuestionTextImportPage = () => {
|
||||
pagination={{ ...tablePagination, showSizeChanger: true }}
|
||||
tableLayout="fixed"
|
||||
scroll={{ x: 1800 }}
|
||||
size="small"
|
||||
onChange={(pagination) =>
|
||||
setTablePagination({
|
||||
current: pagination.current ?? 1,
|
||||
|
||||
@@ -221,6 +221,7 @@ const RecordDetailPage = ({ recordId }: { recordId: string }) => {
|
||||
columns={columns}
|
||||
dataSource={record.answers}
|
||||
rowKey="id"
|
||||
size="small"
|
||||
loading={loading}
|
||||
pagination={{
|
||||
pageSize: 10,
|
||||
|
||||
@@ -314,12 +314,14 @@ const StatisticsPage = () => {
|
||||
columns={overviewColumns}
|
||||
dataSource={records}
|
||||
rowKey="id"
|
||||
size="small"
|
||||
loading={loading}
|
||||
pagination={{
|
||||
pageSize: 10,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
showTotal: (total) => `共 ${total} 条`,
|
||||
size: 'small',
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
@@ -331,11 +333,13 @@ const StatisticsPage = () => {
|
||||
columns={userStatsColumns}
|
||||
dataSource={userStats}
|
||||
rowKey="userId"
|
||||
size="small"
|
||||
pagination={{
|
||||
pageSize: 10,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
showTotal: (total) => `共 ${total} 条`,
|
||||
size: 'small',
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
@@ -347,11 +351,13 @@ const StatisticsPage = () => {
|
||||
columns={subjectStatsColumns}
|
||||
dataSource={subjectStats}
|
||||
rowKey="subjectId"
|
||||
size="small"
|
||||
pagination={{
|
||||
pageSize: 10,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
showTotal: (total) => `共 ${total} 条`,
|
||||
size: 'small',
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
@@ -363,11 +369,13 @@ const StatisticsPage = () => {
|
||||
columns={taskStatsColumns}
|
||||
dataSource={taskStats}
|
||||
rowKey="taskId"
|
||||
size="small"
|
||||
pagination={{
|
||||
pageSize: 10,
|
||||
showSizeChanger: true,
|
||||
showQuickJumper: true,
|
||||
showTotal: (total) => `共 ${total} 条`,
|
||||
size: 'small',
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
@@ -161,8 +161,9 @@ const UserGroupManage = () => {
|
||||
columns={columns}
|
||||
dataSource={groups}
|
||||
rowKey="id"
|
||||
size="small"
|
||||
loading={loading}
|
||||
pagination={{ pageSize: 10 }}
|
||||
pagination={{ pageSize: 10, size: 'small' }}
|
||||
/>
|
||||
|
||||
<Modal
|
||||
|
||||
@@ -45,6 +45,7 @@ const UserManagePage = () => {
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
size: 'small' as const,
|
||||
});
|
||||
// 新增搜索状态
|
||||
const [searchKeyword, setSearchKeyword] = useState<string>('');
|
||||
@@ -57,6 +58,7 @@ const UserManagePage = () => {
|
||||
current: 1,
|
||||
pageSize: 5,
|
||||
total: 0,
|
||||
size: 'small' as const,
|
||||
});
|
||||
// 新增状态:跟踪记录详情
|
||||
const [recordDetailVisible, setRecordDetailVisible] = useState(false);
|
||||
@@ -426,6 +428,7 @@ const UserManagePage = () => {
|
||||
columns={columns}
|
||||
dataSource={users}
|
||||
rowKey="id"
|
||||
size="small"
|
||||
loading={loading}
|
||||
pagination={pagination}
|
||||
onChange={handleTableChange}
|
||||
@@ -499,6 +502,7 @@ const UserManagePage = () => {
|
||||
]}
|
||||
dataSource={userRecords}
|
||||
rowKey="id"
|
||||
size="small"
|
||||
loading={recordsLoading}
|
||||
pagination={recordsPagination}
|
||||
onChange={handleRecordsPaginationChange}
|
||||
|
||||
@@ -22,6 +22,7 @@ const UserRecordsPage = ({ userId }: { userId: string }) => {
|
||||
current: 1,
|
||||
pageSize: 10,
|
||||
total: 0,
|
||||
size: 'small' as const,
|
||||
});
|
||||
|
||||
const fetchRecords = async (page = 1, pageSize = 10) => {
|
||||
@@ -156,6 +157,7 @@ const UserRecordsPage = ({ userId }: { userId: string }) => {
|
||||
columns={columns}
|
||||
dataSource={records}
|
||||
rowKey="id"
|
||||
size="small"
|
||||
loading={loading}
|
||||
pagination={pagination}
|
||||
onChange={handleTableChange}
|
||||
|
||||
@@ -38,12 +38,12 @@ export const OptionList = ({ type, options, value, onChange, disabled }: OptionL
|
||||
return (
|
||||
<div className="mt-3">
|
||||
<TextArea
|
||||
rows={5}
|
||||
rows={4}
|
||||
value={value as string || ''}
|
||||
onChange={(e) => onChange(e.target.value)}
|
||||
placeholder="请输入您的答案..."
|
||||
disabled={disabled}
|
||||
className="rounded-lg border-gray-300 focus:border-[#00897B] focus:ring-[#00897B] text-sm p-3"
|
||||
className="rounded-lg border-gray-300 focus:border-[#00897B] focus:ring-[#00897B] text-xs p-2"
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
@@ -57,7 +57,7 @@ export const OptionList = ({ type, options, value, onChange, disabled }: OptionL
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-2.5 mt-2">
|
||||
<div className="space-y-1.5 mt-2">
|
||||
{renderOptions().map((opt, index) => {
|
||||
const selected = isSelected(opt.value);
|
||||
return (
|
||||
@@ -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-3 rounded-xl border-2 transition-all duration-200 active:scale-[0.99] cursor-pointer
|
||||
relative flex items-start 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-7 h-7 rounded-full flex items-center justify-center text-xs font-medium border mr-2.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 mt-0.5 transition-colors
|
||||
${selected
|
||||
? 'bg-[#00897B] border-[#00897B] text-white'
|
||||
: 'bg-white border-gray-300 text-gray-500'}
|
||||
@@ -82,7 +82,7 @@ export const OptionList = ({ type, options, value, onChange, disabled }: OptionL
|
||||
</div>
|
||||
|
||||
{/* 选项内容 */}
|
||||
<div className={`text-sm leading-snug select-none ${selected ? 'text-[#00695C] font-medium' : 'text-gray-700'}`}>
|
||||
<div className={`text-xs leading-tight select-none ${selected ? 'text-[#00695C] font-medium' : 'text-gray-700'}`}>
|
||||
{opt.label}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -24,23 +24,23 @@ export const QuizFooter = ({
|
||||
const isLast = current === total - 1;
|
||||
|
||||
return (
|
||||
<div className="fixed bottom-0 left-0 right-0 bg-white border-t border-gray-100 px-3 py-2.5 safe-area-bottom z-30 shadow-[0_-2px_10px_rgba(0,0,0,0.05)]">
|
||||
<div className="fixed bottom-0 left-0 right-0 bg-white border-t border-gray-100 px-2 py-1.5 safe-area-bottom z-30 shadow-[0_-2px_10px_rgba(0,0,0,0.05)]">
|
||||
<div className="max-w-md mx-auto flex items-center justify-between">
|
||||
<Button
|
||||
type="text"
|
||||
icon={<LeftOutlined />}
|
||||
onClick={onPrev}
|
||||
disabled={isFirst}
|
||||
className={`flex items-center text-gray-600 hover:text-[#00897B] text-sm ${isFirst ? 'opacity-30' : ''}`}
|
||||
className={`flex items-center text-gray-600 hover:text-[#00897B] text-xs ${isFirst ? 'opacity-30' : ''}`}
|
||||
>
|
||||
上一题
|
||||
</Button>
|
||||
|
||||
<div
|
||||
onClick={onOpenSheet}
|
||||
className="flex flex-col items-center justify-center -mt-6 bg-white rounded-full h-14 w-14 shadow-lg border border-gray-100 cursor-pointer active:scale-95 transition-transform"
|
||||
className="flex flex-col items-center justify-center -mt-4 bg-white rounded-full h-11 w-11 shadow-lg border border-gray-100 cursor-pointer active:scale-95 transition-transform"
|
||||
>
|
||||
<AppstoreOutlined className="text-lg text-[#00897B] mb-0.5" />
|
||||
<AppstoreOutlined className="text-sm text-[#00897B] mb-0.5" />
|
||||
<span className="text-[10px] text-gray-500 scale-90">
|
||||
{answeredCount}/{total}
|
||||
</span>
|
||||
@@ -50,7 +50,7 @@ export const QuizFooter = ({
|
||||
<Button
|
||||
type="text"
|
||||
onClick={onSubmit}
|
||||
className="flex items-center text-[#00897B] font-medium hover:bg-teal-50 text-sm"
|
||||
className="flex items-center text-[#00897B] font-medium hover:bg-teal-50 text-xs"
|
||||
>
|
||||
提交 <RightOutlined />
|
||||
</Button>
|
||||
@@ -58,7 +58,7 @@ export const QuizFooter = ({
|
||||
<Button
|
||||
type="text"
|
||||
onClick={onNext}
|
||||
className="flex items-center text-gray-600 hover:text-[#00897B] text-sm"
|
||||
className="flex items-center text-gray-600 hover:text-[#00897B] text-xs"
|
||||
>
|
||||
下一题 <RightOutlined />
|
||||
</Button>
|
||||
|
||||
@@ -7,9 +7,10 @@ interface QuizHeaderProps {
|
||||
total: number;
|
||||
timeLeft: number | null;
|
||||
onGiveUp: () => void;
|
||||
taskName?: string;
|
||||
}
|
||||
|
||||
export const QuizHeader = ({ current, total, timeLeft, onGiveUp }: QuizHeaderProps) => {
|
||||
export const QuizHeader = ({ current, total, timeLeft, onGiveUp, taskName }: QuizHeaderProps) => {
|
||||
const navigate = useNavigate();
|
||||
|
||||
const formatTime = (seconds: number) => {
|
||||
@@ -19,18 +20,17 @@ export const QuizHeader = ({ current, total, timeLeft, onGiveUp }: QuizHeaderPro
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-[#00897B] text-white px-3 h-12 flex items-center justify-between shadow-md sticky top-0 z-30">
|
||||
<div className="bg-[#00897B] text-white px-2 h-10 flex items-center justify-between shadow-md sticky top-0 z-30">
|
||||
<div className="flex items-center gap-1" onClick={onGiveUp}>
|
||||
<LeftOutlined className="text-base" />
|
||||
<span className="text-sm">返回</span>
|
||||
<LeftOutlined className="text-sm" />
|
||||
<span className="text-xs">返回</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-1.5 bg-[#00796B] px-2 py-0.5 rounded-full text-xs">
|
||||
<EyeOutlined />
|
||||
<span>监控中</span>
|
||||
<div className="flex items-center gap-1 bg-[#00796B] px-1.5 py-0.5 rounded-full text-xs">
|
||||
<span>{taskName || '监控中'}</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-1.5 text-sm font-medium tabular-nums">
|
||||
<div className="flex items-center gap-1 text-xs font-medium tabular-nums">
|
||||
<ClockCircleOutlined />
|
||||
<span>{timeLeft !== null ? formatTime(timeLeft) : '--:--'}</span>
|
||||
</div>
|
||||
|
||||
@@ -9,10 +9,10 @@ export const QuizProgress = ({ current, total }: QuizProgressProps) => {
|
||||
const percent = Math.round((current / total) * 100);
|
||||
|
||||
return (
|
||||
<div className="bg-white px-4 py-3 border-b border-gray-100">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<span className="text-gray-500 text-sm">
|
||||
<span className="text-gray-900 text-lg font-medium">{current}</span>/{total}
|
||||
<div className="bg-white px-2 py-1.5 border-b border-gray-100">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-gray-500 text-xs">
|
||||
<span className="text-gray-900 text-sm font-medium">{current}</span>/{total}
|
||||
</span>
|
||||
<Progress
|
||||
percent={percent}
|
||||
|
||||
Reference in New Issue
Block a user