优化考试管理页面UI和用户体验
- 重构试卷列表页面布局,提升视觉效果 - 优化试卷卡片样式,添加悬停效果和背景装饰 - 改进移动端响应式设计,增强移动设备用户体验 - 统一返回按钮样式,使用一致的图标和文案 - 调整页面间距和对齐方式,提升整体视觉层次 - 优化代码格式,统一引号使用规范 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
74d692ed2c
commit
ebf7c8890a
@ -12,7 +12,7 @@ import {
|
|||||||
message
|
message
|
||||||
} from 'antd'
|
} from 'antd'
|
||||||
import {
|
import {
|
||||||
HomeOutlined
|
LeftOutlined
|
||||||
} from '@ant-design/icons'
|
} from '@ant-design/icons'
|
||||||
import * as examApi from '../api/exam'
|
import * as examApi from '../api/exam'
|
||||||
import type { Question } from '../types/question'
|
import type { Question } from '../types/question'
|
||||||
@ -198,11 +198,10 @@ const ExamAnswerView: React.FC = () => {
|
|||||||
<Row align="middle" justify="space-between">
|
<Row align="middle" justify="space-between">
|
||||||
<Col>
|
<Col>
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
icon={<LeftOutlined />}
|
||||||
icon={<HomeOutlined />}
|
|
||||||
onClick={() => navigate('/exam/management')}
|
onClick={() => navigate('/exam/management')}
|
||||||
>
|
>
|
||||||
返回试卷列表
|
返回
|
||||||
</Button>
|
</Button>
|
||||||
</Col>
|
</Col>
|
||||||
<Col flex="auto" style={{ textAlign: 'center' }}>
|
<Col flex="auto" style={{ textAlign: 'center' }}>
|
||||||
|
|||||||
@ -7,17 +7,14 @@
|
|||||||
.header {
|
.header {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
align-items: flex-start;
|
align-items: center;
|
||||||
margin-bottom: 24px;
|
margin-bottom: 24px;
|
||||||
|
|
||||||
h2 {
|
h2 {
|
||||||
margin: 0 0 4px 0;
|
|
||||||
font-size: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.subtitle {
|
|
||||||
margin: 0;
|
margin: 0;
|
||||||
color: rgba(0, 0, 0, 0.45);
|
font-size: 24px;
|
||||||
|
flex: 1;
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -32,16 +29,22 @@
|
|||||||
font-size: 18px;
|
font-size: 18px;
|
||||||
color: #1890ff;
|
color: #1890ff;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.cardContent {
|
.cardContent {
|
||||||
.description {
|
margin-top: 12px;
|
||||||
margin-bottom: 16px;
|
|
||||||
color: rgba(0, 0, 0, 0.65);
|
.infoRow {
|
||||||
font-size: 14px;
|
display: flex;
|
||||||
|
gap: 16px;
|
||||||
|
margin-bottom: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.statItem {
|
.infoItem {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 6px;
|
gap: 6px;
|
||||||
@ -50,15 +53,97 @@
|
|||||||
|
|
||||||
svg {
|
svg {
|
||||||
color: #1890ff;
|
color: #1890ff;
|
||||||
|
font-size: 14px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.stats {
|
.stats {
|
||||||
margin-top: 16px;
|
|
||||||
padding-top: 16px;
|
|
||||||
border-top: 1px solid #f0f0f0;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statTag {
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 试卷卡片样式优化
|
||||||
|
.examCard {
|
||||||
|
border-radius: 12px;
|
||||||
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||||||
|
border: 1px solid #f0f0f0;
|
||||||
|
transition: all 0.3s ease;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.08);
|
||||||
|
transform: translateY(-2px);
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.ant-card-body) {
|
||||||
|
padding: 16px;
|
||||||
|
position: relative;
|
||||||
|
z-index: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 右侧背景图片
|
||||||
|
&::after {
|
||||||
|
content: '';
|
||||||
|
position: absolute;
|
||||||
|
top: 35px;
|
||||||
|
right: 15px;
|
||||||
|
width: 50px;
|
||||||
|
height: 50px;
|
||||||
|
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%23b8d4f1' stroke-width='1' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z'%3E%3C/path%3E%3Cpolyline points='14,2 14,8 20,8'%3E%3C/polyline%3E%3Cline x1='16' y1='13' x2='8' y2='13'%3E%3C/line%3E%3Cline x1='16' y1='17' x2='8' y2='17'%3E%3C/line%3E%3Cpolyline points='10,9 9,9 8,9'%3E%3C/polyline%3E%3C/svg%3E");
|
||||||
|
background-size: contain;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-position: center;
|
||||||
|
opacity: 0.12;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 卡片操作按钮样式优化
|
||||||
|
:global(.ant-card-actions) {
|
||||||
|
background: #fafafa;
|
||||||
|
|
||||||
|
li {
|
||||||
|
margin: 0 !important;
|
||||||
|
|
||||||
|
button {
|
||||||
|
height: auto;
|
||||||
|
padding: 8px 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.4;
|
||||||
|
white-space: nowrap;
|
||||||
|
|
||||||
|
.anticon {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: rgba(24, 144, 255, 0.05);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
button.ant-btn-link {
|
||||||
|
color: rgba(0, 0, 0, 0.65);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #1890ff;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.ant-btn-dangerous {
|
||||||
|
color: #ff4d4f;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: #ff7875;
|
||||||
|
background: rgba(255, 77, 79, 0.05);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -82,6 +167,7 @@
|
|||||||
|
|
||||||
h2 {
|
h2 {
|
||||||
font-size: 20px;
|
font-size: 20px;
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
@ -91,15 +177,49 @@
|
|||||||
|
|
||||||
.cardTitle {
|
.cardTitle {
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.cardContent {
|
.cardContent {
|
||||||
.description {
|
margin-top: 8px;
|
||||||
|
|
||||||
|
.infoRow {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.infoItem {
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.statItem {
|
.stats {
|
||||||
font-size: 13px;
|
margin-top: 6px;
|
||||||
|
flex-wrap: wrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.statTag {
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移动端卡片操作按钮优化
|
||||||
|
:global(.ant-card-actions) {
|
||||||
|
li {
|
||||||
|
button {
|
||||||
|
padding: 6px 8px;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// 移动端样式调整
|
||||||
|
.examCard::after {
|
||||||
|
display: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -10,8 +10,6 @@ import {
|
|||||||
List,
|
List,
|
||||||
Tag,
|
Tag,
|
||||||
Modal,
|
Modal,
|
||||||
Row,
|
|
||||||
Col,
|
|
||||||
Empty,
|
Empty,
|
||||||
Spin,
|
Spin,
|
||||||
Drawer,
|
Drawer,
|
||||||
@ -27,7 +25,8 @@ import {
|
|||||||
CheckCircleOutlined,
|
CheckCircleOutlined,
|
||||||
TrophyOutlined,
|
TrophyOutlined,
|
||||||
HistoryOutlined,
|
HistoryOutlined,
|
||||||
PrinterOutlined
|
PrinterOutlined,
|
||||||
|
ArrowLeftOutlined
|
||||||
} from '@ant-design/icons'
|
} from '@ant-design/icons'
|
||||||
import * as examApi from '../api/exam'
|
import * as examApi from '../api/exam'
|
||||||
import styles from './ExamManagement.module.less'
|
import styles from './ExamManagement.module.less'
|
||||||
@ -177,10 +176,13 @@ const ExamManagement: React.FC = () => {
|
|||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<Card>
|
<Card>
|
||||||
<div className={styles.header}>
|
<div className={styles.header}>
|
||||||
<div>
|
<Button
|
||||||
<h2>模拟考试</h2>
|
icon={<ArrowLeftOutlined />}
|
||||||
<p className={styles.subtitle}>创建和管理模拟试卷</p>
|
onClick={() => navigate('/')}
|
||||||
</div>
|
>
|
||||||
|
返回
|
||||||
|
</Button>
|
||||||
|
<h2>模拟考试</h2>
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
type="primary"
|
||||||
icon={<PlusOutlined />}
|
icon={<PlusOutlined />}
|
||||||
@ -198,11 +200,12 @@ const ExamManagement: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<List
|
<List
|
||||||
grid={{ gutter: 16, xs: 1, sm: 1, md: 2, lg: 2, xl: 3, xxl: 3 }}
|
grid={{ gutter: 16, xs: 1, sm: 1, md: 1, lg: 2, xl: 2, xxl: 2 }}
|
||||||
dataSource={exams}
|
dataSource={exams}
|
||||||
renderItem={(exam) => (
|
renderItem={(exam) => (
|
||||||
<List.Item>
|
<List.Item>
|
||||||
<Card
|
<Card
|
||||||
|
className={styles.examCard}
|
||||||
hoverable
|
hoverable
|
||||||
actions={[
|
actions={[
|
||||||
<Button
|
<Button
|
||||||
@ -248,32 +251,28 @@ const ExamManagement: React.FC = () => {
|
|||||||
<div className={styles.cardTitle}>
|
<div className={styles.cardTitle}>
|
||||||
<FileTextOutlined />
|
<FileTextOutlined />
|
||||||
<span>{exam.title}</span>
|
<span>{exam.title}</span>
|
||||||
{exam.has_in_progress_exam && (
|
|
||||||
<Tag color="processing" style={{ marginLeft: 8 }}>进行中</Tag>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
description={
|
description={
|
||||||
<div className={styles.cardContent}>
|
<div className={styles.cardContent}>
|
||||||
<Row gutter={[16, 16]}>
|
<div className={styles.infoRow}>
|
||||||
<Col span={12}>
|
<div className={styles.infoItem}>
|
||||||
<div className={styles.statItem}>
|
<ClockCircleOutlined />
|
||||||
<ClockCircleOutlined />
|
<span>{exam.duration} 分钟</span>
|
||||||
<span>{exam.duration} 分钟</span>
|
</div>
|
||||||
</div>
|
<div className={styles.infoItem}>
|
||||||
</Col>
|
<CheckCircleOutlined />
|
||||||
<Col span={12}>
|
<span>及格 {exam.pass_score} 分</span>
|
||||||
<div className={styles.statItem}>
|
</div>
|
||||||
<CheckCircleOutlined />
|
</div>
|
||||||
<span>及格 {exam.pass_score} 分</span>
|
|
||||||
</div>
|
|
||||||
</Col>
|
|
||||||
</Row>
|
|
||||||
<div className={styles.stats}>
|
<div className={styles.stats}>
|
||||||
<Tag icon={<TrophyOutlined />} color="gold">
|
<Tag icon={<TrophyOutlined />} color="gold" className={styles.statTag}>
|
||||||
最高分: {exam.best_score || 0}
|
最高分: {exam.best_score || 0}
|
||||||
</Tag>
|
</Tag>
|
||||||
<Tag color="blue">已考 {exam.attempt_count} 次</Tag>
|
<Tag color="blue" className={styles.statTag}>已考 {exam.attempt_count} 次</Tag>
|
||||||
|
{exam.has_in_progress_exam && (
|
||||||
|
<Tag color="processing" className={styles.statTag}>进行中</Tag>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
import React, { useState, useEffect } from 'react'
|
import React, { useState, useEffect } from "react";
|
||||||
import { useParams, useNavigate } from 'react-router-dom'
|
import { useParams, useNavigate } from "react-router-dom";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
Result,
|
Result,
|
||||||
@ -12,137 +12,139 @@ import {
|
|||||||
Row,
|
Row,
|
||||||
Col,
|
Col,
|
||||||
Statistic,
|
Statistic,
|
||||||
Divider
|
Divider,
|
||||||
} from 'antd'
|
} from "antd";
|
||||||
import {
|
import {
|
||||||
CheckCircleOutlined,
|
CheckCircleOutlined,
|
||||||
CloseCircleOutlined,
|
CloseCircleOutlined,
|
||||||
TrophyOutlined,
|
TrophyOutlined,
|
||||||
ClockCircleOutlined,
|
ClockCircleOutlined,
|
||||||
FileTextOutlined,
|
FileTextOutlined,
|
||||||
HomeOutlined
|
LeftOutlined,
|
||||||
} from '@ant-design/icons'
|
} from "@ant-design/icons";
|
||||||
import * as examApi from '../api/exam'
|
import * as examApi from "../api/exam";
|
||||||
import type { ExamRecordResponse, ExamAnswer } from '../types/exam'
|
import type { ExamRecordResponse, ExamAnswer } from "../types/exam";
|
||||||
import type { Question } from '../types/question'
|
import type { Question } from "../types/question";
|
||||||
import styles from './ExamResultNew.module.less'
|
import styles from "./ExamResultNew.module.less";
|
||||||
|
|
||||||
const { Text, Paragraph } = Typography
|
const { Text, Paragraph } = Typography;
|
||||||
|
|
||||||
// 题型名称映射
|
// 题型名称映射
|
||||||
const TYPE_NAME: Record<string, string> = {
|
const TYPE_NAME: Record<string, string> = {
|
||||||
'fill-in-blank': '填空题',
|
"fill-in-blank": "填空题",
|
||||||
'true-false': '判断题',
|
"true-false": "判断题",
|
||||||
'multiple-choice': '单选题',
|
"multiple-choice": "单选题",
|
||||||
'multiple-selection': '多选题',
|
"multiple-selection": "多选题",
|
||||||
'short-answer': '简答题',
|
"short-answer": "简答题",
|
||||||
'ordinary-essay': '论述题',
|
"ordinary-essay": "论述题",
|
||||||
'management-essay': '论述题',
|
"management-essay": "论述题",
|
||||||
'essay': '论述题' // 合并后的论述题类型
|
essay: "论述题", // 合并后的论述题类型
|
||||||
}
|
};
|
||||||
|
|
||||||
// 题型顺序定义
|
// 题型顺序定义
|
||||||
const TYPE_ORDER: Record<string, number> = {
|
const TYPE_ORDER: Record<string, number> = {
|
||||||
'fill-in-blank': 1,
|
"fill-in-blank": 1,
|
||||||
'true-false': 2,
|
"true-false": 2,
|
||||||
'multiple-choice': 3,
|
"multiple-choice": 3,
|
||||||
'multiple-selection': 4,
|
"multiple-selection": 4,
|
||||||
'short-answer': 5,
|
"short-answer": 5,
|
||||||
'ordinary-essay': 6,
|
"ordinary-essay": 6,
|
||||||
'management-essay': 6,
|
"management-essay": 6,
|
||||||
'essay': 6 // 合并后的论述题顺序
|
essay: 6, // 合并后的论述题顺序
|
||||||
}
|
};
|
||||||
|
|
||||||
const ExamResultNew: React.FC = () => {
|
const ExamResultNew: React.FC = () => {
|
||||||
const { recordId } = useParams<{ recordId: string }>()
|
const { recordId } = useParams<{ recordId: string }>();
|
||||||
const navigate = useNavigate()
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true);
|
||||||
const [data, setData] = useState<ExamRecordResponse | null>(null)
|
const [data, setData] = useState<ExamRecordResponse | null>(null);
|
||||||
const [questions, setQuestions] = useState<Question[]>([])
|
const [questions, setQuestions] = useState<Question[]>([]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!recordId) {
|
if (!recordId) {
|
||||||
message.error('参数错误')
|
message.error("参数错误");
|
||||||
navigate('/exam/management')
|
navigate("/exam/management");
|
||||||
return
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
loadResult()
|
loadResult();
|
||||||
}, [recordId])
|
}, [recordId]);
|
||||||
|
|
||||||
const loadResult = async () => {
|
const loadResult = async () => {
|
||||||
setLoading(true)
|
setLoading(true);
|
||||||
try {
|
try {
|
||||||
const recordRes = await examApi.getExamRecord(Number(recordId))
|
const recordRes = await examApi.getExamRecord(Number(recordId));
|
||||||
|
|
||||||
if (recordRes.success && recordRes.data) {
|
if (recordRes.success && recordRes.data) {
|
||||||
setData(recordRes.data)
|
setData(recordRes.data);
|
||||||
|
|
||||||
// 获取试卷详情
|
// 获取试卷详情
|
||||||
if (recordRes.data.record.exam?.id) {
|
if (recordRes.data.record.exam?.id) {
|
||||||
const examRes = await examApi.getExamDetail(recordRes.data.record.exam.id)
|
const examRes = await examApi.getExamDetail(
|
||||||
|
recordRes.data.record.exam.id
|
||||||
|
);
|
||||||
if (examRes.success && examRes.data) {
|
if (examRes.success && examRes.data) {
|
||||||
setQuestions(examRes.data.questions)
|
setQuestions(examRes.data.questions);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
message.error('加载结果失败')
|
message.error("加载结果失败");
|
||||||
navigate('/exam/management')
|
navigate("/exam/management");
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
message.error(error.response?.data?.message || '加载结果失败')
|
message.error(error.response?.data?.message || "加载结果失败");
|
||||||
navigate('/exam/management')
|
navigate("/exam/management");
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false)
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className={styles.loadingContainer}>
|
<div className={styles.loadingContainer}>
|
||||||
<Spin size="large" />
|
<Spin size="large" />
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!data) {
|
if (!data) {
|
||||||
return null
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { record, answers } = data
|
const { record, answers } = data;
|
||||||
const isPassed = record.is_passed
|
const isPassed = record.is_passed;
|
||||||
// 总分统一为100分
|
// 总分统一为100分
|
||||||
const scorePercent = record.score
|
const scorePercent = record.score;
|
||||||
|
|
||||||
// 构建答案映射
|
// 构建答案映射
|
||||||
const answerMap = new Map<number, ExamAnswer>()
|
const answerMap = new Map<number, ExamAnswer>();
|
||||||
answers.forEach(ans => {
|
answers.forEach((ans) => {
|
||||||
answerMap.set(ans.question_id, ans)
|
answerMap.set(ans.question_id, ans);
|
||||||
})
|
});
|
||||||
|
|
||||||
// 统计正确率
|
// 统计正确率
|
||||||
const correctCount = answers.filter(a => a.is_correct).length
|
const correctCount = answers.filter((a) => a.is_correct).length;
|
||||||
const totalCount = answers.length
|
const totalCount = answers.length;
|
||||||
const correctRate = totalCount > 0 ? (correctCount / totalCount) * 100 : 0
|
const correctRate = totalCount > 0 ? (correctCount / totalCount) * 100 : 0;
|
||||||
|
|
||||||
// 格式化时间
|
// 格式化时间
|
||||||
const formatTime = (seconds: number) => {
|
const formatTime = (seconds: number) => {
|
||||||
const totalSeconds = Math.floor(seconds) // 确保是整数
|
const totalSeconds = Math.floor(seconds); // 确保是整数
|
||||||
const minutes = Math.floor(totalSeconds / 60)
|
const minutes = Math.floor(totalSeconds / 60);
|
||||||
const secs = totalSeconds % 60
|
const secs = totalSeconds % 60;
|
||||||
return `${minutes}分${secs}秒`
|
return `${minutes}分${secs}秒`;
|
||||||
}
|
};
|
||||||
|
|
||||||
// 渲染答案详情
|
// 渲染答案详情
|
||||||
const renderAnswerDetail = (question: Question, answer: ExamAnswer) => {
|
const renderAnswerDetail = (question: Question, answer: ExamAnswer) => {
|
||||||
const isCorrect = answer.is_correct
|
const isCorrect = answer.is_correct;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.answerDetail}>
|
<div className={styles.answerDetail}>
|
||||||
{/* 题目内容 - 填空题特殊处理 */}
|
{/* 题目内容 - 填空题特殊处理 */}
|
||||||
<div className={styles.questionContent}>
|
<div className={styles.questionContent}>
|
||||||
{question.type === 'fill-in-blank' ? (
|
{question.type === "fill-in-blank" ? (
|
||||||
<Paragraph strong style={{ fontSize: 16, marginBottom: 16 }}>
|
<Paragraph strong style={{ fontSize: 16, marginBottom: 16 }}>
|
||||||
{renderFillInBlankQuestion(question.content)}
|
{renderFillInBlankQuestion(question.content)}
|
||||||
</Paragraph>
|
</Paragraph>
|
||||||
@ -154,18 +156,21 @@ const ExamResultNew: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className={styles.answerSection}>
|
<div className={styles.answerSection}>
|
||||||
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
|
<Space direction="vertical" size="middle" style={{ width: "100%" }}>
|
||||||
{/* 用户答案 */}
|
{/* 用户答案 */}
|
||||||
<div className={styles.answerItem}>
|
<div className={styles.answerItem}>
|
||||||
<Space>
|
<Space>
|
||||||
<Text type="secondary">你的答案:</Text>
|
<Text type="secondary">你的答案:</Text>
|
||||||
<Text strong className={isCorrect ? styles.correct : styles.incorrect}>
|
<Text
|
||||||
|
strong
|
||||||
|
className={isCorrect ? styles.correct : styles.incorrect}
|
||||||
|
>
|
||||||
{formatAnswer(answer.answer, question.type)}
|
{formatAnswer(answer.answer, question.type)}
|
||||||
</Text>
|
</Text>
|
||||||
{isCorrect ? (
|
{isCorrect ? (
|
||||||
<CheckCircleOutlined style={{ color: '#52c41a' }} />
|
<CheckCircleOutlined style={{ color: "#52c41a" }} />
|
||||||
) : (
|
) : (
|
||||||
<CloseCircleOutlined style={{ color: '#ff4d4f' }} />
|
<CloseCircleOutlined style={{ color: "#ff4d4f" }} />
|
||||||
)}
|
)}
|
||||||
</Space>
|
</Space>
|
||||||
</div>
|
</div>
|
||||||
@ -174,7 +179,7 @@ const ExamResultNew: React.FC = () => {
|
|||||||
<div className={styles.answerItem}>
|
<div className={styles.answerItem}>
|
||||||
<Space>
|
<Space>
|
||||||
<Text type="secondary">正确答案:</Text>
|
<Text type="secondary">正确答案:</Text>
|
||||||
<Text strong style={{ color: '#52c41a' }}>
|
<Text strong style={{ color: "#52c41a" }}>
|
||||||
{formatAnswer(answer.correct_answer, question.type)}
|
{formatAnswer(answer.correct_answer, question.type)}
|
||||||
</Text>
|
</Text>
|
||||||
</Space>
|
</Space>
|
||||||
@ -184,7 +189,13 @@ const ExamResultNew: React.FC = () => {
|
|||||||
<div className={styles.answerItem}>
|
<div className={styles.answerItem}>
|
||||||
<Space>
|
<Space>
|
||||||
<Text type="secondary">得分:</Text>
|
<Text type="secondary">得分:</Text>
|
||||||
<Text strong style={{ color: isCorrect ? '#52c41a' : '#ff4d4f', fontSize: 16 }}>
|
<Text
|
||||||
|
strong
|
||||||
|
style={{
|
||||||
|
color: isCorrect ? "#52c41a" : "#ff4d4f",
|
||||||
|
fontSize: 16,
|
||||||
|
}}
|
||||||
|
>
|
||||||
{answer.score.toFixed(1)} 分
|
{answer.score.toFixed(1)} 分
|
||||||
</Text>
|
</Text>
|
||||||
</Space>
|
</Space>
|
||||||
@ -192,11 +203,25 @@ const ExamResultNew: React.FC = () => {
|
|||||||
|
|
||||||
{/* AI评分详情 */}
|
{/* AI评分详情 */}
|
||||||
{answer.ai_grading && (
|
{answer.ai_grading && (
|
||||||
<div className={styles.aiGrading} style={{ marginTop: 12, padding: 16, background: '#f0f5ff', borderRadius: 8 }}>
|
<div
|
||||||
|
className={styles.aiGrading}
|
||||||
|
style={{
|
||||||
|
marginTop: 12,
|
||||||
|
padding: 16,
|
||||||
|
background: "#f0f5ff",
|
||||||
|
borderRadius: 8,
|
||||||
|
}}
|
||||||
|
>
|
||||||
<div style={{ marginBottom: 8 }}>
|
<div style={{ marginBottom: 8 }}>
|
||||||
<Text strong style={{ color: '#1890ff' }}>AI评分详情:</Text>
|
<Text strong style={{ color: "#1890ff" }}>
|
||||||
|
AI评分详情:
|
||||||
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
<Space direction="vertical" size="small" style={{ width: '100%' }}>
|
<Space
|
||||||
|
direction="vertical"
|
||||||
|
size="small"
|
||||||
|
style={{ width: "100%" }}
|
||||||
|
>
|
||||||
<div>
|
<div>
|
||||||
<Text type="secondary">AI得分:</Text>
|
<Text type="secondary">AI得分:</Text>
|
||||||
<Text strong>{answer.ai_grading.score} / 100</Text>
|
<Text strong>{answer.ai_grading.score} / 100</Text>
|
||||||
@ -217,79 +242,97 @@ const ExamResultNew: React.FC = () => {
|
|||||||
</Space>
|
</Space>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
// 渲染填空题题目(将 **** 替换为下划线)
|
// 渲染填空题题目(将 **** 替换为下划线)
|
||||||
const renderFillInBlankQuestion = (content: string) => {
|
const renderFillInBlankQuestion = (content: string) => {
|
||||||
const parts = content.split('****')
|
const parts = content.split("****");
|
||||||
return (
|
return (
|
||||||
<span>
|
<span>
|
||||||
{parts.map((part, i) => (
|
{parts.map((part, i) => (
|
||||||
<React.Fragment key={i}>
|
<React.Fragment key={i}>
|
||||||
{part}
|
{part}
|
||||||
{i < parts.length - 1 && (
|
{i < parts.length - 1 && (
|
||||||
<span style={{
|
<span
|
||||||
display: 'inline-block',
|
style={{
|
||||||
minWidth: '120px',
|
display: "inline-block",
|
||||||
borderBottom: '2px solid #1890ff',
|
minWidth: "120px",
|
||||||
marginLeft: 8,
|
borderBottom: "2px solid #1890ff",
|
||||||
marginRight: 8
|
marginLeft: 8,
|
||||||
}}>
|
marginRight: 8,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
</React.Fragment>
|
</React.Fragment>
|
||||||
))}
|
))}
|
||||||
</span>
|
</span>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
// 格式化用户答案
|
// 格式化用户答案
|
||||||
const formatAnswer = (answer: any, type: string): string => {
|
const formatAnswer = (answer: any, type: string): string => {
|
||||||
if (answer === null || answer === undefined || answer === '') {
|
if (answer === null || answer === undefined || answer === "") {
|
||||||
return '未作答'
|
return "未作答";
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Array.isArray(answer)) {
|
if (Array.isArray(answer)) {
|
||||||
if (answer.length === 0) return '未作答'
|
if (answer.length === 0) return "未作答";
|
||||||
return answer.filter(a => a !== null && a !== undefined && a !== '').join('、')
|
return answer
|
||||||
|
.filter((a) => a !== null && a !== undefined && a !== "")
|
||||||
|
.join("、");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (type === 'true-false') {
|
if (type === "true-false") {
|
||||||
// 处理判断题:支持字符串和布尔值
|
// 处理判断题:支持字符串和布尔值
|
||||||
const answerStr = String(answer).toLowerCase()
|
const answerStr = String(answer).toLowerCase();
|
||||||
return answerStr === 'true' ? '正确' : '错误'
|
return answerStr === "true" ? "正确" : "错误";
|
||||||
}
|
}
|
||||||
|
|
||||||
return String(answer)
|
return String(answer);
|
||||||
}
|
};
|
||||||
|
|
||||||
// 按题型分组(合并两种论述题)
|
// 按题型分组(合并两种论述题)
|
||||||
const groupedQuestions = questions.reduce((acc, q) => {
|
const groupedQuestions = questions.reduce((acc, q) => {
|
||||||
// 将两种论述题统一为 'essay'
|
// 将两种论述题统一为 'essay'
|
||||||
const displayType = (q.type === 'ordinary-essay' || q.type === 'management-essay') ? 'essay' : q.type
|
const displayType =
|
||||||
|
q.type === "ordinary-essay" || q.type === "management-essay"
|
||||||
|
? "essay"
|
||||||
|
: q.type;
|
||||||
if (!acc[displayType]) {
|
if (!acc[displayType]) {
|
||||||
acc[displayType] = []
|
acc[displayType] = [];
|
||||||
}
|
}
|
||||||
acc[displayType].push(q)
|
acc[displayType].push(q);
|
||||||
return acc
|
return acc;
|
||||||
}, {} as Record<string, Question[]>)
|
}, {} as Record<string, Question[]>);
|
||||||
|
|
||||||
// 计算各题型得分(已在 groupedQuestions 中合并论述题)
|
// 计算各题型得分(已在 groupedQuestions 中合并论述题)
|
||||||
const typeScores = Object.entries(groupedQuestions)
|
const typeScores = Object.entries(groupedQuestions)
|
||||||
.map(([type, qs]) => {
|
.map(([type, qs]) => {
|
||||||
const typeAnswers = qs.map(q => answerMap.get(q.id)).filter(Boolean) as ExamAnswer[]
|
const typeAnswers = qs
|
||||||
const totalScore = typeAnswers.reduce((sum, ans) => sum + ans.score, 0)
|
.map((q) => answerMap.get(q.id))
|
||||||
const maxScore = typeAnswers.length * (
|
.filter(Boolean) as ExamAnswer[];
|
||||||
type === 'fill-in-blank' ? 2.0 :
|
const totalScore = typeAnswers.reduce((sum, ans) => sum + ans.score, 0);
|
||||||
type === 'true-false' ? 2.0 :
|
const maxScore =
|
||||||
type === 'multiple-choice' ? 1.0 :
|
typeAnswers.length *
|
||||||
type === 'multiple-selection' ? 2.5 :
|
(type === "fill-in-blank"
|
||||||
type === 'short-answer' ? 10.0 :
|
? 2.0
|
||||||
(type === 'essay' || type === 'ordinary-essay' || type === 'management-essay') ? 5.0 : 0
|
: type === "true-false"
|
||||||
)
|
? 2.0
|
||||||
const correctCount = typeAnswers.filter(ans => ans.is_correct).length
|
: type === "multiple-choice"
|
||||||
|
? 1.0
|
||||||
|
: type === "multiple-selection"
|
||||||
|
? 2.5
|
||||||
|
: type === "short-answer"
|
||||||
|
? 10.0
|
||||||
|
: type === "essay" ||
|
||||||
|
type === "ordinary-essay" ||
|
||||||
|
type === "management-essay"
|
||||||
|
? 5.0
|
||||||
|
: 0);
|
||||||
|
const correctCount = typeAnswers.filter((ans) => ans.is_correct).length;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
type,
|
type,
|
||||||
@ -298,21 +341,21 @@ const ExamResultNew: React.FC = () => {
|
|||||||
maxScore,
|
maxScore,
|
||||||
correctCount,
|
correctCount,
|
||||||
totalCount: typeAnswers.length,
|
totalCount: typeAnswers.length,
|
||||||
order: TYPE_ORDER[type] || 999
|
order: TYPE_ORDER[type] || 999,
|
||||||
}
|
};
|
||||||
})
|
})
|
||||||
.sort((a, b) => a.order - b.order)
|
.sort((a, b) => a.order - b.order);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
{/* 成绩展示 */}
|
{/* 成绩展示 */}
|
||||||
<Result
|
<Result
|
||||||
status={isPassed ? 'success' : 'warning'}
|
status={isPassed ? "success" : "warning"}
|
||||||
title={isPassed ? '恭喜你,考试通过!' : '很遗憾,未通过考试'}
|
title={isPassed ? "恭喜你,考试通过!" : "很遗憾,未通过考试"}
|
||||||
subTitle={
|
subTitle={
|
||||||
<Space direction="vertical" size="large">
|
<Space direction="vertical" size="large">
|
||||||
<Text style={{ fontSize: 16 }}>
|
<Text style={{ fontSize: 16 }}>
|
||||||
{record.exam?.title || '模拟考试'}
|
{record.exam?.title || "模拟考试"}
|
||||||
</Text>
|
</Text>
|
||||||
</Space>
|
</Space>
|
||||||
}
|
}
|
||||||
@ -327,7 +370,10 @@ const ExamResultNew: React.FC = () => {
|
|||||||
value={scorePercent.toFixed(1)}
|
value={scorePercent.toFixed(1)}
|
||||||
suffix="/ 100"
|
suffix="/ 100"
|
||||||
prefix={<TrophyOutlined />}
|
prefix={<TrophyOutlined />}
|
||||||
valueStyle={{ color: isPassed ? '#52c41a' : '#ff4d4f', fontSize: 32 }}
|
valueStyle={{
|
||||||
|
color: isPassed ? "#52c41a" : "#ff4d4f",
|
||||||
|
fontSize: 32,
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
<Col xs={24} sm={12} md={6}>
|
<Col xs={24} sm={12} md={6}>
|
||||||
@ -336,7 +382,7 @@ const ExamResultNew: React.FC = () => {
|
|||||||
value={correctRate.toFixed(1)}
|
value={correctRate.toFixed(1)}
|
||||||
suffix="%"
|
suffix="%"
|
||||||
prefix={<CheckCircleOutlined />}
|
prefix={<CheckCircleOutlined />}
|
||||||
valueStyle={{ color: '#1890ff', fontSize: 32 }}
|
valueStyle={{ color: "#1890ff", fontSize: 32 }}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
<Col xs={24} sm={12} md={6}>
|
<Col xs={24} sm={12} md={6}>
|
||||||
@ -348,14 +394,29 @@ const ExamResultNew: React.FC = () => {
|
|||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
<Col xs={24} sm={12} md={6}>
|
<Col xs={24} sm={12} md={6}>
|
||||||
<div style={{ textAlign: 'center' }}>
|
<div style={{ textAlign: "center" }}>
|
||||||
<div style={{ color: 'rgba(0, 0, 0, 0.45)', fontSize: 14, marginBottom: 8 }}>
|
<div
|
||||||
|
style={{
|
||||||
|
color: "rgba(0, 0, 0, 0.45)",
|
||||||
|
fontSize: 14,
|
||||||
|
marginBottom: 8,
|
||||||
|
}}
|
||||||
|
>
|
||||||
考试状态
|
考试状态
|
||||||
</div>
|
</div>
|
||||||
<Tag color={isPassed ? 'success' : 'error'} style={{ fontSize: 16, padding: '4px 16px' }}>
|
<Tag
|
||||||
{isPassed ? '已通过' : '未通过'}
|
color={isPassed ? "success" : "error"}
|
||||||
|
style={{ fontSize: 16, padding: "4px 16px" }}
|
||||||
|
>
|
||||||
|
{isPassed ? "已通过" : "未通过"}
|
||||||
</Tag>
|
</Tag>
|
||||||
<div style={{ marginTop: 8, color: 'rgba(0, 0, 0, 0.45)', fontSize: 12 }}>
|
<div
|
||||||
|
style={{
|
||||||
|
marginTop: 8,
|
||||||
|
color: "rgba(0, 0, 0, 0.45)",
|
||||||
|
fontSize: 12,
|
||||||
|
}}
|
||||||
|
>
|
||||||
及格分数:{record.exam?.pass_score || 60} 分
|
及格分数:{record.exam?.pass_score || 60} 分
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -365,15 +426,21 @@ const ExamResultNew: React.FC = () => {
|
|||||||
|
|
||||||
{/* 各题型得分情况 */}
|
{/* 各题型得分情况 */}
|
||||||
<Card
|
<Card
|
||||||
title={<Text strong style={{ fontSize: 18 }}>题型得分统计</Text>}
|
title={
|
||||||
|
<Text strong style={{ fontSize: 18 }}>
|
||||||
|
题型得分统计
|
||||||
|
</Text>
|
||||||
|
}
|
||||||
className={styles.typeScoreCard}
|
className={styles.typeScoreCard}
|
||||||
>
|
>
|
||||||
<Row gutter={[16, 16]}>
|
<Row gutter={[16, 16]}>
|
||||||
{typeScores.map(ts => (
|
{typeScores.map((ts) => (
|
||||||
<Col xs={24} sm={12} md={8} lg={6} key={ts.type}>
|
<Col xs={24} sm={12} md={8} lg={6} key={ts.type}>
|
||||||
<div className={styles.typeScoreItem}>
|
<div className={styles.typeScoreItem}>
|
||||||
<div className={styles.typeScoreHeader}>
|
<div className={styles.typeScoreHeader}>
|
||||||
<Text strong style={{ fontSize: 16 }}>{ts.typeName}</Text>
|
<Text strong style={{ fontSize: 16 }}>
|
||||||
|
{ts.typeName}
|
||||||
|
</Text>
|
||||||
<Text type="secondary" style={{ fontSize: 12 }}>
|
<Text type="secondary" style={{ fontSize: 12 }}>
|
||||||
({ts.correctCount}/{ts.totalCount}题正确)
|
({ts.correctCount}/{ts.totalCount}题正确)
|
||||||
</Text>
|
</Text>
|
||||||
@ -383,12 +450,16 @@ const ExamResultNew: React.FC = () => {
|
|||||||
strong
|
strong
|
||||||
style={{
|
style={{
|
||||||
fontSize: 28,
|
fontSize: 28,
|
||||||
color: ts.totalScore === ts.maxScore ? '#52c41a' : '#1890ff'
|
color:
|
||||||
|
ts.totalScore === ts.maxScore ? "#52c41a" : "#1890ff",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{ts.totalScore.toFixed(1)}
|
{ts.totalScore.toFixed(1)}
|
||||||
</Text>
|
</Text>
|
||||||
<Text type="secondary" style={{ fontSize: 16, marginLeft: 4 }}>
|
<Text
|
||||||
|
type="secondary"
|
||||||
|
style={{ fontSize: 16, marginLeft: 4 }}
|
||||||
|
>
|
||||||
/ {ts.maxScore.toFixed(1)}
|
/ {ts.maxScore.toFixed(1)}
|
||||||
</Text>
|
</Text>
|
||||||
</div>
|
</div>
|
||||||
@ -397,7 +468,8 @@ const ExamResultNew: React.FC = () => {
|
|||||||
className={styles.typeScoreBar}
|
className={styles.typeScoreBar}
|
||||||
style={{
|
style={{
|
||||||
width: `${(ts.totalScore / ts.maxScore) * 100}%`,
|
width: `${(ts.totalScore / ts.maxScore) * 100}%`,
|
||||||
background: ts.totalScore === ts.maxScore ? '#52c41a' : '#1890ff'
|
background:
|
||||||
|
ts.totalScore === ts.maxScore ? "#52c41a" : "#1890ff",
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -408,68 +480,84 @@ const ExamResultNew: React.FC = () => {
|
|||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* 答题详情 - 直接展示,不使用折叠 */}
|
{/* 答题详情 - 直接展示,不使用折叠 */}
|
||||||
<Card title={<Text strong style={{ fontSize: 18 }}>答题详情</Text>} className={styles.detailCard}>
|
<Card
|
||||||
|
title={
|
||||||
|
<Text strong style={{ fontSize: 18 }}>
|
||||||
|
答题详情
|
||||||
|
</Text>
|
||||||
|
}
|
||||||
|
className={styles.detailCard}
|
||||||
|
>
|
||||||
{Object.entries(groupedQuestions)
|
{Object.entries(groupedQuestions)
|
||||||
.sort(([typeA], [typeB]) => {
|
.sort(([typeA], [typeB]) => {
|
||||||
const orderA = TYPE_ORDER[typeA] || 999
|
const orderA = TYPE_ORDER[typeA] || 999;
|
||||||
const orderB = TYPE_ORDER[typeB] || 999
|
const orderB = TYPE_ORDER[typeB] || 999;
|
||||||
return orderA - orderB
|
return orderA - orderB;
|
||||||
})
|
})
|
||||||
.map(([type, qs]) => (
|
.map(([type, qs]) => (
|
||||||
<div key={type} style={{ marginBottom: 32 }}>
|
<div key={type} style={{ marginBottom: 32 }}>
|
||||||
{/* 题型标题 */}
|
{/* 题型标题 */}
|
||||||
<div style={{
|
<div
|
||||||
padding: '12px 16px',
|
style={{
|
||||||
background: '#fafafa',
|
padding: "12px 16px",
|
||||||
borderLeft: '4px solid #1890ff',
|
background: "#fafafa",
|
||||||
marginBottom: 16
|
borderLeft: "4px solid #1890ff",
|
||||||
}}>
|
marginBottom: 16,
|
||||||
<Space>
|
}}
|
||||||
<Text strong style={{ fontSize: 16 }}>{TYPE_NAME[type] || type}</Text>
|
>
|
||||||
<Text type="secondary">(共 {qs.length} 题)</Text>
|
<Space>
|
||||||
|
<Text strong style={{ fontSize: 16 }}>
|
||||||
|
{TYPE_NAME[type] || type}
|
||||||
|
</Text>
|
||||||
|
<Text type="secondary">(共 {qs.length} 题)</Text>
|
||||||
|
</Space>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 题目列表 */}
|
||||||
|
<Space
|
||||||
|
direction="vertical"
|
||||||
|
size="large"
|
||||||
|
style={{ width: "100%" }}
|
||||||
|
>
|
||||||
|
{qs.map((q, idx) => {
|
||||||
|
const ans = answerMap.get(q.id);
|
||||||
|
if (!ans) return null;
|
||||||
|
return (
|
||||||
|
<Card
|
||||||
|
key={q.id}
|
||||||
|
size="small"
|
||||||
|
className={styles.questionCard}
|
||||||
|
style={{
|
||||||
|
borderLeft: ans.is_correct
|
||||||
|
? "4px solid #52c41a"
|
||||||
|
: "4px solid #ff4d4f",
|
||||||
|
background: ans.is_correct ? "#f6ffed" : "#fff2f0",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div style={{ marginBottom: 12 }}>
|
||||||
|
<Tag color="blue">第 {idx + 1} 题</Tag>
|
||||||
|
</div>
|
||||||
|
{renderAnswerDetail(q, ans)}
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</Space>
|
</Space>
|
||||||
|
|
||||||
|
{/* 题型之间的分隔线 */}
|
||||||
|
<Divider />
|
||||||
</div>
|
</div>
|
||||||
|
))}
|
||||||
{/* 题目列表 */}
|
|
||||||
<Space direction="vertical" size="large" style={{ width: '100%' }}>
|
|
||||||
{qs.map((q, idx) => {
|
|
||||||
const ans = answerMap.get(q.id)
|
|
||||||
if (!ans) return null
|
|
||||||
return (
|
|
||||||
<Card
|
|
||||||
key={q.id}
|
|
||||||
size="small"
|
|
||||||
className={styles.questionCard}
|
|
||||||
style={{
|
|
||||||
borderLeft: ans.is_correct ? '4px solid #52c41a' : '4px solid #ff4d4f',
|
|
||||||
background: ans.is_correct ? '#f6ffed' : '#fff2f0'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div style={{ marginBottom: 12 }}>
|
|
||||||
<Tag color="blue">第 {idx + 1} 题</Tag>
|
|
||||||
</div>
|
|
||||||
{renderAnswerDetail(q, ans)}
|
|
||||||
</Card>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</Space>
|
|
||||||
|
|
||||||
{/* 题型之间的分隔线 */}
|
|
||||||
<Divider />
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
{/* 操作按钮 */}
|
{/* 操作按钮 */}
|
||||||
<Card className={styles.actionsCard}>
|
<Card className={styles.actionsCard}>
|
||||||
<Space size="large">
|
<Space size="large">
|
||||||
<Button
|
<Button
|
||||||
type="primary"
|
|
||||||
size="large"
|
size="large"
|
||||||
icon={<HomeOutlined />}
|
icon={<LeftOutlined />}
|
||||||
onClick={() => navigate('/exam/management')}
|
onClick={() => navigate("/exam/management")}
|
||||||
>
|
>
|
||||||
返回试卷列表
|
返回
|
||||||
</Button>
|
</Button>
|
||||||
{record.exam?.id && (
|
{record.exam?.id && (
|
||||||
<Button
|
<Button
|
||||||
@ -477,12 +565,14 @@ const ExamResultNew: React.FC = () => {
|
|||||||
icon={<FileTextOutlined />}
|
icon={<FileTextOutlined />}
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
try {
|
try {
|
||||||
const res = await examApi.startExam(record.exam!.id)
|
const res = await examApi.startExam(record.exam!.id);
|
||||||
if (res.success && res.data) {
|
if (res.success && res.data) {
|
||||||
navigate(`/exam/${record.exam!.id}/taking/${res.data.record_id}`)
|
navigate(
|
||||||
|
`/exam/${record.exam!.id}/taking/${res.data.record_id}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
message.error('开始考试失败')
|
message.error("开始考试失败");
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -492,7 +582,7 @@ const ExamResultNew: React.FC = () => {
|
|||||||
</Space>
|
</Space>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
};
|
||||||
|
|
||||||
export default ExamResultNew
|
export default ExamResultNew;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user