feat: 添加得分占比计算功能;优化时间格式化工具函数;更新考试记录和用户任务页面以显示得分占比;新增得分占比测试用例;修复时间解析逻辑
This commit is contained in:
@@ -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">
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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>,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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: '状态',
|
||||
|
||||
Reference in New Issue
Block a user