feat: 优化用户任务页面和管理页面的表格样式

调整字体大小和列宽,增强可读性;更新管理员登录页面的样式,调整按钮和文本大小;修复考试科目页面、考试任务页面、问题管理页面等多个页面的表格分页样式,统一设置为小尺寸;更新选项列表和测验组件的样式,提升用户体验。
This commit is contained in:
2025-12-29 16:51:59 +08:00
parent 46cd96a6be
commit 03eb858749
26 changed files with 490 additions and 352 deletions

View File

@@ -1,4 +1,4 @@
# 宝来威天问平台
# 宝来威考试平台
功能完善的在线问卷调查系统,支持多种题型、随机抽题、免注册答题等特性。

Binary file not shown.

View File

@@ -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>

View File

@@ -68,7 +68,7 @@
@media (max-width: 768px) {
.container {
padding: 0 16px;
padding: 0px;
}
}

View File

@@ -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>
&copy; {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>

View File

@@ -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',
},
}
}}
>

View File

@@ -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">
&copy; {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>
);

View File

@@ -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"
>

View File

@@ -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>

View File

@@ -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>

View File

@@ -460,6 +460,7 @@ const AdminDashboardPage = () => {
total: taskStatsPagination.total,
showSizeChanger: false,
onChange: (page) => fetchTaskStats(page),
size: 'small',
}}
/>
</Card>

View File

@@ -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">
&copy; {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>
);

View File

@@ -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',
}}
/>

View File

@@ -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>

View File

@@ -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,

View File

@@ -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

View File

@@ -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,

View File

@@ -221,6 +221,7 @@ const RecordDetailPage = ({ recordId }: { recordId: string }) => {
columns={columns}
dataSource={record.answers}
rowKey="id"
size="small"
loading={loading}
pagination={{
pageSize: 10,

View File

@@ -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>

View File

@@ -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

View File

@@ -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}

View File

@@ -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}

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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}