feat: 添加得分占比计算功能;优化时间格式化工具函数;更新考试记录和用户任务页面以显示得分占比;新增得分占比测试用例;修复时间解析逻辑

This commit is contained in:
2025-12-30 15:26:53 +08:00
parent 8cd6950631
commit 1822d8b4da
18 changed files with 347 additions and 68 deletions

View File

@@ -5,6 +5,7 @@ import { useNavigate } from 'react-router-dom';
import { useUser } from '../contexts';
import { examSubjectAPI, examTaskAPI, quizAPI } from '../services/api';
import { UserLayout } from '../layouts/UserLayout';
import { formatDate, parseUtcDateTime } from '../utils/validation';
const { Title, Text } = Typography;
@@ -67,8 +68,8 @@ export const SubjectSelectionPage: React.FC = () => {
const getTaskStatus = (task: ExamTask) => {
const nowMs = Date.now();
const startMs = new Date(task.startAt).getTime();
const endMs = new Date(task.endAt).getTime();
const startMs = parseUtcDateTime(task.startAt)?.getTime() ?? NaN;
const endMs = parseUtcDateTime(task.endAt)?.getTime() ?? NaN;
const usedAttempts = Number(task.usedAttempts) || 0;
const maxAttempts = Number(task.maxAttempts) || 3;
@@ -232,7 +233,7 @@ export const SubjectSelectionPage: React.FC = () => {
<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()}
{formatDate(task.startAt)} - {formatDate(task.endAt)}
</Text>
</div>
<div className="text-green-600">
@@ -265,7 +266,7 @@ export const SubjectSelectionPage: React.FC = () => {
const usedAttempts = Number(task.usedAttempts) || 0;
const maxAttempts = Number(task.maxAttempts) || 3;
const attemptsExhausted = usedAttempts >= maxAttempts;
const endMs = new Date(task.endAt).getTime();
const endMs = parseUtcDateTime(task.endAt)?.getTime() ?? NaN;
const isExpired = Number.isFinite(endMs) ? endMs < Date.now() : true;
const isDisabled = attemptsExhausted || isExpired;
return (
@@ -308,7 +309,7 @@ export const SubjectSelectionPage: React.FC = () => {
<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()}
{formatDate(task.startAt)} - {formatDate(task.endAt)}
</Text>
</div>
{attemptsExhausted ? (
@@ -385,7 +386,7 @@ export const SubjectSelectionPage: React.FC = () => {
<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()}
{formatDate(task.startAt)} - {formatDate(task.endAt)}
</Text>
</div>
<div className="text-blue-600">

View File

@@ -5,6 +5,7 @@ import { useNavigate } from 'react-router-dom';
import { useUser } from '../contexts';
import { quizAPI } from '../services/api';
import { UserLayout } from '../layouts/UserLayout';
import { formatDateTime } from '../utils/validation';
const { Title, Text } = Typography;
@@ -138,7 +139,7 @@ export const UserTaskPage: React.FC = () => {
<Space size={4}>
<CalendarOutlined style={{ fontSize: '11px' }} className="text-gray-500" />
<Text type="secondary" style={{ fontSize: '11px' }}>
{new Date(date).toLocaleString('zh-CN')}
{formatDateTime(date)}
</Text>
</Space>
)

View File

@@ -9,7 +9,7 @@ import {
ReloadOutlined,
} from '@ant-design/icons';
import { adminAPI, quizAPI } from '../../services/api';
import { formatDateTime } from '../../utils/validation';
import { formatDateTime, parseUtcDateTime } from '../../utils/validation';
import type { Dayjs } from 'dayjs';
import {
PieChart,
@@ -258,8 +258,8 @@ const AdminDashboardPage = () => {
key: 'progress',
render: (_: any, record: ActiveTaskStat) => {
const now = new Date();
const start = new Date(record.startAt);
const end = new Date(record.endAt);
const start = parseUtcDateTime(record.startAt) ?? new Date(record.startAt);
const end = parseUtcDateTime(record.endAt) ?? new Date(record.endAt);
const totalDuration = end.getTime() - start.getTime();
const elapsedDuration = now.getTime() - start.getTime();

View File

@@ -3,6 +3,7 @@ import { Table, Button, Input, Space, message, Popconfirm, Modal, Form, InputNum
import { PlusOutlined, EditOutlined, DeleteOutlined, EyeOutlined } from '@ant-design/icons';
import api from '../../services/api';
import { getCategoryColorHex } from '../../lib/categoryColors';
import { parseUtcDateTime } from '../../utils/validation';
interface Question {
id: string;
@@ -417,7 +418,8 @@ const ExamSubjectPage = () => {
dataIndex: 'createdAt',
key: 'createdAt',
render: (text: string) => {
const date = new Date(text);
const date = parseUtcDateTime(text) ?? new Date(text);
if (Number.isNaN(date.getTime())) return text;
return (
<div>
<div>{date.toLocaleDateString()}</div>

View File

@@ -3,6 +3,7 @@ import { Table, Button, Input, Space, message, Popconfirm, Modal, Form, DatePick
import { PlusOutlined, EditOutlined, DeleteOutlined, FileTextOutlined } from '@ant-design/icons';
import api, { userGroupAPI } from '../../services/api';
import dayjs from 'dayjs';
import { formatDateTime, parseUtcDateTime } from '../../utils/validation';
interface ExamTask {
id: string;
@@ -148,8 +149,8 @@ const ExamTaskPage = () => {
form.setFieldsValue({
name: task.name,
subjectId: task.subjectId,
startAt: dayjs(task.startAt),
endAt: dayjs(task.endAt),
startAt: dayjs(parseUtcDateTime(task.startAt) ?? task.startAt),
endAt: dayjs(parseUtcDateTime(task.endAt) ?? task.endAt),
userIds: userIds,
groupIds: groupIds,
});
@@ -228,14 +229,14 @@ const ExamTaskPage = () => {
dataIndex: 'startAt',
key: 'startAt',
width: 110,
render: (text: string) => dayjs(text).format('YYYY-MM-DD HH:mm'),
render: (text: string) => formatDateTime(text),
},
{
title: '结束时间',
dataIndex: 'endAt',
key: 'endAt',
width: 110,
render: (text: string) => dayjs(text).format('YYYY-MM-DD HH:mm'),
render: (text: string) => formatDateTime(text),
},
{
title: '考试进程',
@@ -244,8 +245,8 @@ const ExamTaskPage = () => {
width: 160,
render: (_: any, record: ExamTask) => {
const now = dayjs();
const start = dayjs(record.startAt);
const end = dayjs(record.endAt);
const start = dayjs(parseUtcDateTime(record.startAt) ?? record.startAt);
const end = dayjs(parseUtcDateTime(record.endAt) ?? record.endAt);
let progress = 0;
if (now < start) {
@@ -306,7 +307,7 @@ const ExamTaskPage = () => {
dataIndex: 'createdAt',
key: 'createdAt',
width: 110,
render: (text: string) => dayjs(text).format('YYYY-MM-DD HH:mm'),
render: (text: string) => formatDateTime(text),
},
{
title: <div style={{ textAlign: 'center' }}></div>,
@@ -523,11 +524,20 @@ const ExamTaskPage = () => {
key: 'score',
render: (score: number | null) => score !== null ? `${score}` : '未答题',
},
{
title: '得分占比',
dataIndex: 'scorePercentage',
key: 'scorePercentage',
render: (pct: number | null) => {
if (pct === null || pct === undefined || Number.isNaN(Number(pct))) return '未答题';
return `${Math.round(Number(pct) * 10) / 10}%`;
},
},
{
title: '完成时间',
dataIndex: 'completedAt',
key: 'completedAt',
render: (date: string | null) => date ? dayjs(date).format('YYYY-MM-DD HH:mm') : '未答题',
render: (date: string | null) => (date ? formatDateTime(date) : '未答题'),
},
]}
dataSource={reportData.details}

View File

@@ -4,6 +4,7 @@ import { PlusOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons';
import { useLocation } from 'react-router-dom';
import api from '../../services/api';
import { getCategoryColorHex } from '../../lib/categoryColors';
import { formatDateTime } from '../../utils/validation';
interface QuestionCategory {
id: string;
@@ -118,7 +119,7 @@ const QuestionCategoryPage = () => {
width: 200,
dataIndex: 'createdAt',
key: 'createdAt',
render: (text: string) => new Date(text).toLocaleString(),
render: (text: string) => formatDateTime(text, { includeSeconds: true }),
},
{
title: <div style={{ textAlign: 'center' }}></div>,

View File

@@ -33,7 +33,7 @@ import {
import * as XLSX from 'xlsx';
import { useNavigate } from 'react-router-dom';
import { questionAPI } from '../../services/api';
import { questionTypeMap, questionTypeColors } from '../../utils/validation';
import { formatDate, questionTypeMap, questionTypeColors } from '../../utils/validation';
import { getCategoryColorHex } from '../../lib/categoryColors';
const { Option } = Select;
@@ -428,7 +428,7 @@ const QuestionManagePage = () => {
dataIndex: 'createdAt',
key: 'createdAt',
width: 160,
render: (date: string) => new Date(date).toLocaleDateString(),
render: (date: string) => formatDate(date),
},
{
title: <div style={{ textAlign: 'center' }}></div>,

View File

@@ -2,7 +2,7 @@ import React, { useState, useEffect } from 'react';
import { Table, Card, Row, Col, Statistic, Button, message } from 'antd';
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, PieChart, Pie, Cell } from 'recharts';
import api from '../../services/api';
import dayjs from 'dayjs';
import { formatDateTime } from '../../utils/validation';
interface Answer {
id: string;
@@ -154,7 +154,7 @@ const RecordDetailPage = ({ recordId }: { recordId: string }) => {
<div className="mb-6">
<h1 className="text-2xl font-bold text-gray-800"></h1>
<p className="text-gray-600 mt-2">
{dayjs(record.createdAt).format('YYYY-MM-DD HH:mm:ss')}
{formatDateTime(record.createdAt, { includeSeconds: true })}
</p>
</div>

View File

@@ -2,7 +2,7 @@ import React, { useState, useEffect } from 'react';
import { Table, Button, Input, Space, message, Popconfirm, Modal, Form, Tag } from 'antd';
import { PlusOutlined, EditOutlined, DeleteOutlined, TeamOutlined } from '@ant-design/icons';
import { userGroupAPI } from '../../services/api';
import dayjs from 'dayjs';
import { formatDateTime } from '../../utils/validation';
interface UserGroup {
id: string;
@@ -116,7 +116,7 @@ const UserGroupManage = () => {
title: '创建时间',
dataIndex: 'createdAt',
key: 'createdAt',
render: (text: string) => dayjs(text).format('YYYY-MM-DD HH:mm'),
render: (text: string) => formatDateTime(text),
},
{
title: <div style={{ textAlign: 'center' }}></div>,

View File

@@ -1,3 +1,4 @@
import { formatDateTime } from '../../utils/validation';
import React, { useState, useEffect } from 'react';
import { Table, Button, Input, Space, message, Popconfirm, Modal, Form, Switch, Upload, Tabs, Select, Tag } from 'antd';
import { PlusOutlined, EditOutlined, DeleteOutlined, ExportOutlined, ImportOutlined, EyeOutlined, EyeInvisibleOutlined } from '@ant-design/icons';
@@ -127,24 +128,22 @@ const UserManagePage = () => {
fetchUsers(newPagination.current, newPagination.pageSize, searchKeyword);
};
// 处理搜索
const handleSearch = () => {
fetchUsers(1, pagination.pageSize, searchKeyword);
};
// 处理搜索框变化
const handleSearchChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setSearchKeyword(e.target.value);
};
const handleCreate = async () => {
const groups = await fetchUserGroups();
setEditingUser(null);
form.resetFields();
const groups = await fetchUserGroups();
const systemGroups = groups.filter((g) => g.isSystem).map((g) => g.id);
form.setFieldsValue({ groupIds: systemGroups });
if (systemGroups.length) {
form.setFieldsValue({ groupIds: systemGroups });
}
setModalVisible(true);
};
@@ -367,13 +366,13 @@ const UserManagePage = () => {
title: '最后一次考试时间',
dataIndex: 'lastExamTime',
key: 'lastExamTime',
render: (time: string) => time ? new Date(time).toLocaleString() : '无',
render: (time: string) => (time ? formatDateTime(time, { includeSeconds: true }) : '无'),
},
{
title: '注册时间',
dataIndex: 'createdAt',
key: 'createdAt',
render: (text: string) => new Date(text).toLocaleString(),
render: (text: string) => formatDateTime(text, { includeSeconds: true }),
},
{
title: <div style={{ textAlign: 'center' }}></div>,
@@ -522,7 +521,7 @@ const UserManagePage = () => {
title: '考试时间',
dataIndex: 'createdAt',
key: 'createdAt',
render: (time: string) => new Date(time).toLocaleString(),
render: (time: string) => formatDateTime(time, { includeSeconds: true }),
},
]}
dataSource={userRecords}
@@ -621,7 +620,7 @@ const UserManagePage = () => {
</div>
<div className="col-span-2">
<label className="text-gray-600 font-medium"></label>
<span>{new Date(recordDetail.createdAt).toLocaleString()}</span>
<span>{formatDateTime(recordDetail.createdAt, { includeSeconds: true })}</span>
</div>
</div>
</div>

View File

@@ -2,7 +2,7 @@ import React, { useState, useEffect } from 'react';
import { Table, Card, Row, Col, Statistic, Button, message } from 'antd';
import { BarChart, Bar, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, PieChart, Pie, Cell } from 'recharts';
import api from '../../services/api';
import dayjs from 'dayjs';
import { formatDateTime } from '../../utils/validation';
interface Record {
id: string;
@@ -77,7 +77,7 @@ const UserRecordsPage = ({ userId }: { userId: string }) => {
title: '答题时间',
dataIndex: 'createdAt',
key: 'createdAt',
render: (text: string) => dayjs(text).format('YYYY-MM-DD HH:mm:ss'),
render: (text: string) => formatDateTime(text, { includeSeconds: true }),
},
{
title: '状态',