优化考试管理页面UI和用户体验

- 重构试卷列表页面布局,提升视觉效果
- 优化试卷卡片样式,添加悬停效果和背景装饰
- 改进移动端响应式设计,增强移动设备用户体验
- 统一返回按钮样式,使用一致的图标和文案
- 调整页面间距和对齐方式,提升整体视觉层次
- 优化代码格式,统一引号使用规范

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
yanlongqi 2025-11-18 11:34:09 +08:00
parent 74d692ed2c
commit ebf7c8890a
4 changed files with 445 additions and 237 deletions

View File

@ -12,7 +12,7 @@ import {
message
} from 'antd'
import {
HomeOutlined
LeftOutlined
} from '@ant-design/icons'
import * as examApi from '../api/exam'
import type { Question } from '../types/question'
@ -198,11 +198,10 @@ const ExamAnswerView: React.FC = () => {
<Row align="middle" justify="space-between">
<Col>
<Button
type="primary"
icon={<HomeOutlined />}
icon={<LeftOutlined />}
onClick={() => navigate('/exam/management')}
>
</Button>
</Col>
<Col flex="auto" style={{ textAlign: 'center' }}>

View File

@ -7,17 +7,14 @@
.header {
display: flex;
justify-content: space-between;
align-items: flex-start;
align-items: center;
margin-bottom: 24px;
h2 {
margin: 0 0 4px 0;
font-size: 24px;
}
.subtitle {
margin: 0;
color: rgba(0, 0, 0, 0.45);
font-size: 24px;
flex: 1;
text-align: center;
}
}
@ -32,16 +29,22 @@
font-size: 18px;
color: #1890ff;
}
span {
line-height: 1.4;
}
}
.cardContent {
.description {
margin-bottom: 16px;
color: rgba(0, 0, 0, 0.65);
font-size: 14px;
margin-top: 12px;
.infoRow {
display: flex;
gap: 16px;
margin-bottom: 12px;
}
.statItem {
.infoItem {
display: flex;
align-items: center;
gap: 6px;
@ -50,15 +53,97 @@
svg {
color: #1890ff;
font-size: 14px;
}
}
.stats {
margin-top: 16px;
padding-top: 16px;
border-top: 1px solid #f0f0f0;
display: flex;
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 {
font-size: 20px;
text-align: center;
}
button {
@ -91,15 +177,49 @@
.cardTitle {
font-size: 15px;
margin-bottom: 8px;
svg {
font-size: 16px;
}
}
.cardContent {
.description {
margin-top: 8px;
.infoRow {
flex-direction: column;
gap: 8px;
margin-bottom: 8px;
}
.infoItem {
font-size: 13px;
}
.statItem {
font-size: 13px;
.stats {
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;
}
}

View File

@ -10,8 +10,6 @@ import {
List,
Tag,
Modal,
Row,
Col,
Empty,
Spin,
Drawer,
@ -27,7 +25,8 @@ import {
CheckCircleOutlined,
TrophyOutlined,
HistoryOutlined,
PrinterOutlined
PrinterOutlined,
ArrowLeftOutlined
} from '@ant-design/icons'
import * as examApi from '../api/exam'
import styles from './ExamManagement.module.less'
@ -177,10 +176,13 @@ const ExamManagement: React.FC = () => {
<div className={styles.container}>
<Card>
<div className={styles.header}>
<div>
<h2></h2>
<p className={styles.subtitle}></p>
</div>
<Button
icon={<ArrowLeftOutlined />}
onClick={() => navigate('/')}
>
</Button>
<h2></h2>
<Button
type="primary"
icon={<PlusOutlined />}
@ -198,11 +200,12 @@ const ExamManagement: React.FC = () => {
/>
) : (
<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}
renderItem={(exam) => (
<List.Item>
<Card
className={styles.examCard}
hoverable
actions={[
<Button
@ -248,32 +251,28 @@ const ExamManagement: React.FC = () => {
<div className={styles.cardTitle}>
<FileTextOutlined />
<span>{exam.title}</span>
{exam.has_in_progress_exam && (
<Tag color="processing" style={{ marginLeft: 8 }}></Tag>
)}
</div>
}
description={
<div className={styles.cardContent}>
<Row gutter={[16, 16]}>
<Col span={12}>
<div className={styles.statItem}>
<ClockCircleOutlined />
<span>{exam.duration} </span>
</div>
</Col>
<Col span={12}>
<div className={styles.statItem}>
<CheckCircleOutlined />
<span> {exam.pass_score} </span>
</div>
</Col>
</Row>
<div className={styles.infoRow}>
<div className={styles.infoItem}>
<ClockCircleOutlined />
<span>{exam.duration} </span>
</div>
<div className={styles.infoItem}>
<CheckCircleOutlined />
<span> {exam.pass_score} </span>
</div>
</div>
<div className={styles.stats}>
<Tag icon={<TrophyOutlined />} color="gold">
<Tag icon={<TrophyOutlined />} color="gold" className={styles.statTag}>
: {exam.best_score || 0}
</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>
}

View File

@ -1,5 +1,5 @@
import React, { useState, useEffect } from 'react'
import { useParams, useNavigate } from 'react-router-dom'
import React, { useState, useEffect } from "react";
import { useParams, useNavigate } from "react-router-dom";
import {
Card,
Result,
@ -12,137 +12,139 @@ import {
Row,
Col,
Statistic,
Divider
} from 'antd'
Divider,
} from "antd";
import {
CheckCircleOutlined,
CloseCircleOutlined,
TrophyOutlined,
ClockCircleOutlined,
FileTextOutlined,
HomeOutlined
} from '@ant-design/icons'
import * as examApi from '../api/exam'
import type { ExamRecordResponse, ExamAnswer } from '../types/exam'
import type { Question } from '../types/question'
import styles from './ExamResultNew.module.less'
LeftOutlined,
} from "@ant-design/icons";
import * as examApi from "../api/exam";
import type { ExamRecordResponse, ExamAnswer } from "../types/exam";
import type { Question } from "../types/question";
import styles from "./ExamResultNew.module.less";
const { Text, Paragraph } = Typography
const { Text, Paragraph } = Typography;
// 题型名称映射
const TYPE_NAME: Record<string, string> = {
'fill-in-blank': '填空题',
'true-false': '判断题',
'multiple-choice': '单选题',
'multiple-selection': '多选题',
'short-answer': '简答题',
'ordinary-essay': '论述题',
'management-essay': '论述题',
'essay': '论述题' // 合并后的论述题类型
}
"fill-in-blank": "填空题",
"true-false": "判断题",
"multiple-choice": "单选题",
"multiple-selection": "多选题",
"short-answer": "简答题",
"ordinary-essay": "论述题",
"management-essay": "论述题",
essay: "论述题", // 合并后的论述题类型
};
// 题型顺序定义
const TYPE_ORDER: Record<string, number> = {
'fill-in-blank': 1,
'true-false': 2,
'multiple-choice': 3,
'multiple-selection': 4,
'short-answer': 5,
'ordinary-essay': 6,
'management-essay': 6,
'essay': 6 // 合并后的论述题顺序
}
"fill-in-blank": 1,
"true-false": 2,
"multiple-choice": 3,
"multiple-selection": 4,
"short-answer": 5,
"ordinary-essay": 6,
"management-essay": 6,
essay: 6, // 合并后的论述题顺序
};
const ExamResultNew: React.FC = () => {
const { recordId } = useParams<{ recordId: string }>()
const navigate = useNavigate()
const { recordId } = useParams<{ recordId: string }>();
const navigate = useNavigate();
const [loading, setLoading] = useState(true)
const [data, setData] = useState<ExamRecordResponse | null>(null)
const [questions, setQuestions] = useState<Question[]>([])
const [loading, setLoading] = useState(true);
const [data, setData] = useState<ExamRecordResponse | null>(null);
const [questions, setQuestions] = useState<Question[]>([]);
useEffect(() => {
if (!recordId) {
message.error('参数错误')
navigate('/exam/management')
return
message.error("参数错误");
navigate("/exam/management");
return;
}
loadResult()
}, [recordId])
loadResult();
}, [recordId]);
const loadResult = async () => {
setLoading(true)
setLoading(true);
try {
const recordRes = await examApi.getExamRecord(Number(recordId))
const recordRes = await examApi.getExamRecord(Number(recordId));
if (recordRes.success && recordRes.data) {
setData(recordRes.data)
setData(recordRes.data);
// 获取试卷详情
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) {
setQuestions(examRes.data.questions)
setQuestions(examRes.data.questions);
}
}
} else {
message.error('加载结果失败')
navigate('/exam/management')
message.error("加载结果失败");
navigate("/exam/management");
}
} catch (error: any) {
message.error(error.response?.data?.message || '加载结果失败')
navigate('/exam/management')
message.error(error.response?.data?.message || "加载结果失败");
navigate("/exam/management");
} finally {
setLoading(false)
setLoading(false);
}
}
};
if (loading) {
return (
<div className={styles.loadingContainer}>
<Spin size="large" />
</div>
)
);
}
if (!data) {
return null
return null;
}
const { record, answers } = data
const isPassed = record.is_passed
const { record, answers } = data;
const isPassed = record.is_passed;
// 总分统一为100分
const scorePercent = record.score
const scorePercent = record.score;
// 构建答案映射
const answerMap = new Map<number, ExamAnswer>()
answers.forEach(ans => {
answerMap.set(ans.question_id, ans)
})
const answerMap = new Map<number, ExamAnswer>();
answers.forEach((ans) => {
answerMap.set(ans.question_id, ans);
});
// 统计正确率
const correctCount = answers.filter(a => a.is_correct).length
const totalCount = answers.length
const correctRate = totalCount > 0 ? (correctCount / totalCount) * 100 : 0
const correctCount = answers.filter((a) => a.is_correct).length;
const totalCount = answers.length;
const correctRate = totalCount > 0 ? (correctCount / totalCount) * 100 : 0;
// 格式化时间
const formatTime = (seconds: number) => {
const totalSeconds = Math.floor(seconds) // 确保是整数
const minutes = Math.floor(totalSeconds / 60)
const secs = totalSeconds % 60
return `${minutes}${secs}`
}
const totalSeconds = Math.floor(seconds); // 确保是整数
const minutes = Math.floor(totalSeconds / 60);
const secs = totalSeconds % 60;
return `${minutes}${secs}`;
};
// 渲染答案详情
const renderAnswerDetail = (question: Question, answer: ExamAnswer) => {
const isCorrect = answer.is_correct
const isCorrect = answer.is_correct;
return (
<div className={styles.answerDetail}>
{/* 题目内容 - 填空题特殊处理 */}
<div className={styles.questionContent}>
{question.type === 'fill-in-blank' ? (
{question.type === "fill-in-blank" ? (
<Paragraph strong style={{ fontSize: 16, marginBottom: 16 }}>
{renderFillInBlankQuestion(question.content)}
</Paragraph>
@ -154,18 +156,21 @@ const ExamResultNew: React.FC = () => {
</div>
<div className={styles.answerSection}>
<Space direction="vertical" size="middle" style={{ width: '100%' }}>
<Space direction="vertical" size="middle" style={{ width: "100%" }}>
{/* 用户答案 */}
<div className={styles.answerItem}>
<Space>
<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)}
</Text>
{isCorrect ? (
<CheckCircleOutlined style={{ color: '#52c41a' }} />
<CheckCircleOutlined style={{ color: "#52c41a" }} />
) : (
<CloseCircleOutlined style={{ color: '#ff4d4f' }} />
<CloseCircleOutlined style={{ color: "#ff4d4f" }} />
)}
</Space>
</div>
@ -174,7 +179,7 @@ const ExamResultNew: React.FC = () => {
<div className={styles.answerItem}>
<Space>
<Text type="secondary"></Text>
<Text strong style={{ color: '#52c41a' }}>
<Text strong style={{ color: "#52c41a" }}>
{formatAnswer(answer.correct_answer, question.type)}
</Text>
</Space>
@ -184,7 +189,13 @@ const ExamResultNew: React.FC = () => {
<div className={styles.answerItem}>
<Space>
<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)}
</Text>
</Space>
@ -192,11 +203,25 @@ const ExamResultNew: React.FC = () => {
{/* AI评分详情 */}
{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 }}>
<Text strong style={{ color: '#1890ff' }}>AI评分详情</Text>
<Text strong style={{ color: "#1890ff" }}>
AI评分详情
</Text>
</div>
<Space direction="vertical" size="small" style={{ width: '100%' }}>
<Space
direction="vertical"
size="small"
style={{ width: "100%" }}
>
<div>
<Text type="secondary">AI得分</Text>
<Text strong>{answer.ai_grading.score} / 100</Text>
@ -217,79 +242,97 @@ const ExamResultNew: React.FC = () => {
</Space>
</div>
</div>
)
}
);
};
// 渲染填空题题目(将 **** 替换为下划线)
const renderFillInBlankQuestion = (content: string) => {
const parts = content.split('****')
const parts = content.split("****");
return (
<span>
{parts.map((part, i) => (
<React.Fragment key={i}>
{part}
{i < parts.length - 1 && (
<span style={{
display: 'inline-block',
minWidth: '120px',
borderBottom: '2px solid #1890ff',
marginLeft: 8,
marginRight: 8
}}>
<span
style={{
display: "inline-block",
minWidth: "120px",
borderBottom: "2px solid #1890ff",
marginLeft: 8,
marginRight: 8,
}}
>
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;
</span>
)}
</React.Fragment>
))}
</span>
)
}
);
};
// 格式化用户答案
const formatAnswer = (answer: any, type: string): string => {
if (answer === null || answer === undefined || answer === '') {
return '未作答'
if (answer === null || answer === undefined || answer === "") {
return "未作答";
}
if (Array.isArray(answer)) {
if (answer.length === 0) return '未作答'
return answer.filter(a => a !== null && a !== undefined && a !== '').join('、')
if (answer.length === 0) return "未作答";
return answer
.filter((a) => a !== null && a !== undefined && a !== "")
.join("、");
}
if (type === 'true-false') {
if (type === "true-false") {
// 处理判断题:支持字符串和布尔值
const answerStr = String(answer).toLowerCase()
return answerStr === 'true' ? '正确' : '错误'
const answerStr = String(answer).toLowerCase();
return answerStr === "true" ? "正确" : "错误";
}
return String(answer)
}
return String(answer);
};
// 按题型分组(合并两种论述题)
const groupedQuestions = questions.reduce((acc, q) => {
// 将两种论述题统一为 '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]) {
acc[displayType] = []
acc[displayType] = [];
}
acc[displayType].push(q)
return acc
}, {} as Record<string, Question[]>)
acc[displayType].push(q);
return acc;
}, {} as Record<string, Question[]>);
// 计算各题型得分(已在 groupedQuestions 中合并论述题)
const typeScores = Object.entries(groupedQuestions)
.map(([type, qs]) => {
const typeAnswers = qs.map(q => answerMap.get(q.id)).filter(Boolean) as ExamAnswer[]
const totalScore = typeAnswers.reduce((sum, ans) => sum + ans.score, 0)
const maxScore = typeAnswers.length * (
type === 'fill-in-blank' ? 2.0 :
type === 'true-false' ? 2.0 :
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
const typeAnswers = qs
.map((q) => answerMap.get(q.id))
.filter(Boolean) as ExamAnswer[];
const totalScore = typeAnswers.reduce((sum, ans) => sum + ans.score, 0);
const maxScore =
typeAnswers.length *
(type === "fill-in-blank"
? 2.0
: type === "true-false"
? 2.0
: 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 {
type,
@ -298,21 +341,21 @@ const ExamResultNew: React.FC = () => {
maxScore,
correctCount,
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 (
<div className={styles.container}>
{/* 成绩展示 */}
<Result
status={isPassed ? 'success' : 'warning'}
title={isPassed ? '恭喜你,考试通过!' : '很遗憾,未通过考试'}
status={isPassed ? "success" : "warning"}
title={isPassed ? "恭喜你,考试通过!" : "很遗憾,未通过考试"}
subTitle={
<Space direction="vertical" size="large">
<Text style={{ fontSize: 16 }}>
{record.exam?.title || '模拟考试'}
{record.exam?.title || "模拟考试"}
</Text>
</Space>
}
@ -327,7 +370,10 @@ const ExamResultNew: React.FC = () => {
value={scorePercent.toFixed(1)}
suffix="/ 100"
prefix={<TrophyOutlined />}
valueStyle={{ color: isPassed ? '#52c41a' : '#ff4d4f', fontSize: 32 }}
valueStyle={{
color: isPassed ? "#52c41a" : "#ff4d4f",
fontSize: 32,
}}
/>
</Col>
<Col xs={24} sm={12} md={6}>
@ -336,7 +382,7 @@ const ExamResultNew: React.FC = () => {
value={correctRate.toFixed(1)}
suffix="%"
prefix={<CheckCircleOutlined />}
valueStyle={{ color: '#1890ff', fontSize: 32 }}
valueStyle={{ color: "#1890ff", fontSize: 32 }}
/>
</Col>
<Col xs={24} sm={12} md={6}>
@ -348,14 +394,29 @@ const ExamResultNew: React.FC = () => {
/>
</Col>
<Col xs={24} sm={12} md={6}>
<div style={{ textAlign: 'center' }}>
<div style={{ color: 'rgba(0, 0, 0, 0.45)', fontSize: 14, marginBottom: 8 }}>
<div style={{ textAlign: "center" }}>
<div
style={{
color: "rgba(0, 0, 0, 0.45)",
fontSize: 14,
marginBottom: 8,
}}
>
</div>
<Tag color={isPassed ? 'success' : 'error'} style={{ fontSize: 16, padding: '4px 16px' }}>
{isPassed ? '已通过' : '未通过'}
<Tag
color={isPassed ? "success" : "error"}
style={{ fontSize: 16, padding: "4px 16px" }}
>
{isPassed ? "已通过" : "未通过"}
</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}
</div>
</div>
@ -365,15 +426,21 @@ const ExamResultNew: React.FC = () => {
{/* 各题型得分情况 */}
<Card
title={<Text strong style={{ fontSize: 18 }}></Text>}
title={
<Text strong style={{ fontSize: 18 }}>
</Text>
}
className={styles.typeScoreCard}
>
<Row gutter={[16, 16]}>
{typeScores.map(ts => (
{typeScores.map((ts) => (
<Col xs={24} sm={12} md={8} lg={6} key={ts.type}>
<div className={styles.typeScoreItem}>
<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 }}>
{ts.correctCount}/{ts.totalCount}
</Text>
@ -383,12 +450,16 @@ const ExamResultNew: React.FC = () => {
strong
style={{
fontSize: 28,
color: ts.totalScore === ts.maxScore ? '#52c41a' : '#1890ff'
color:
ts.totalScore === ts.maxScore ? "#52c41a" : "#1890ff",
}}
>
{ts.totalScore.toFixed(1)}
</Text>
<Text type="secondary" style={{ fontSize: 16, marginLeft: 4 }}>
<Text
type="secondary"
style={{ fontSize: 16, marginLeft: 4 }}
>
/ {ts.maxScore.toFixed(1)}
</Text>
</div>
@ -397,7 +468,8 @@ const ExamResultNew: React.FC = () => {
className={styles.typeScoreBar}
style={{
width: `${(ts.totalScore / ts.maxScore) * 100}%`,
background: ts.totalScore === ts.maxScore ? '#52c41a' : '#1890ff'
background:
ts.totalScore === ts.maxScore ? "#52c41a" : "#1890ff",
}}
/>
</div>
@ -408,68 +480,84 @@ const ExamResultNew: React.FC = () => {
</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)
.sort(([typeA], [typeB]) => {
const orderA = TYPE_ORDER[typeA] || 999
const orderB = TYPE_ORDER[typeB] || 999
return orderA - orderB
const orderA = TYPE_ORDER[typeA] || 999;
const orderB = TYPE_ORDER[typeB] || 999;
return orderA - orderB;
})
.map(([type, qs]) => (
<div key={type} style={{ marginBottom: 32 }}>
{/* 题型标题 */}
<div style={{
padding: '12px 16px',
background: '#fafafa',
borderLeft: '4px solid #1890ff',
marginBottom: 16
}}>
<Space>
<Text strong style={{ fontSize: 16 }}>{TYPE_NAME[type] || type}</Text>
<Text type="secondary"> {qs.length} </Text>
<div key={type} style={{ marginBottom: 32 }}>
{/* 题型标题 */}
<div
style={{
padding: "12px 16px",
background: "#fafafa",
borderLeft: "4px solid #1890ff",
marginBottom: 16,
}}
>
<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>
{/* 题型之间的分隔线 */}
<Divider />
</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 className={styles.actionsCard}>
<Space size="large">
<Button
type="primary"
size="large"
icon={<HomeOutlined />}
onClick={() => navigate('/exam/management')}
icon={<LeftOutlined />}
onClick={() => navigate("/exam/management")}
>
</Button>
{record.exam?.id && (
<Button
@ -477,12 +565,14 @@ const ExamResultNew: React.FC = () => {
icon={<FileTextOutlined />}
onClick={async () => {
try {
const res = await examApi.startExam(record.exam!.id)
const res = await examApi.startExam(record.exam!.id);
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) {
message.error('开始考试失败')
message.error("开始考试失败");
}
}}
>
@ -492,7 +582,7 @@ const ExamResultNew: React.FC = () => {
</Space>
</Card>
</div>
)
}
);
};
export default ExamResultNew
export default ExamResultNew;