parent
4adf262a8a
commit
c3318b1259
7 changed files with 364 additions and 49 deletions
@ -0,0 +1,144 @@ |
|||||||
|
import React, { useState, useEffect } from 'react'; |
||||||
|
import {Row, Col, Typography, Button} from 'antd'; |
||||||
|
import ReactECharts from 'echarts-for-react'; |
||||||
|
import QuestionComponent from './QuestionComponent'; |
||||||
|
import { getPaperName, getQuestionAccuracyData, getQuestions } from 'api/exam-online/index'; |
||||||
|
import { withRouter, RouteComponentProps } from 'react-router-dom'; // 引入 withRouter 和 RouteComponentProps
|
||||||
|
import { SingleQuestionProps } from './QuestionComponent'; |
||||||
|
import ESBreadcrumbComponent from "./ESBreadcrumbComponent"; |
||||||
|
|
||||||
|
const { Title } = Typography; |
||||||
|
|
||||||
|
// 接口返回的数据结构,包含准确率和数量(这里数量每个都是 1)
|
||||||
|
interface QuestionAccuracyItem { |
||||||
|
accuracy: number; |
||||||
|
count: number; |
||||||
|
} |
||||||
|
|
||||||
|
interface PaperData { |
||||||
|
paperName: string; |
||||||
|
questionAccuracyData: QuestionAccuracyItem[]; |
||||||
|
} |
||||||
|
|
||||||
|
const ExamPaperAnalysisDetailPage: React.FC<RouteComponentProps> = ({history}) => { |
||||||
|
const [paperData, setPaperData] = useState<PaperData | null>(null); |
||||||
|
const [tableData, setTableData] = useState<SingleQuestionProps[]>([]); |
||||||
|
|
||||||
|
useEffect(() => { |
||||||
|
const fetchData = async () => { |
||||||
|
try { |
||||||
|
const id = sessionStorage.getItem("paperId"); |
||||||
|
sessionStorage.removeItem("paperId"); |
||||||
|
const paperName = await getPaperName(id); |
||||||
|
const questionAccuracyData = await getQuestionAccuracyData(id); |
||||||
|
setPaperData({ paperName, questionAccuracyData }); |
||||||
|
const questions = await getQuestions(id); |
||||||
|
setTableData(questions); |
||||||
|
} catch (error) { |
||||||
|
console.error('数据获取失败:', error); |
||||||
|
} |
||||||
|
}; |
||||||
|
|
||||||
|
fetchData(); |
||||||
|
}, []); |
||||||
|
const handleGoBack = () => { |
||||||
|
history.push("/exam-paper-analysis"); |
||||||
|
}; |
||||||
|
|
||||||
|
|
||||||
|
const getBarChartOption = (data: QuestionAccuracyItem[]) => { |
||||||
|
// 初始化每个区间的题目数量
|
||||||
|
const countGroupedData = [0, 0, 0, 0]; |
||||||
|
let totalCount = 0; |
||||||
|
|
||||||
|
// 统计每个区间的题目数量和总数量
|
||||||
|
data.forEach(item => { |
||||||
|
if (item.accuracy < 0.1) { |
||||||
|
countGroupedData[0] += item.count; |
||||||
|
} else if (item.accuracy >= 0.1 && item.accuracy < 0.6) { |
||||||
|
countGroupedData[1] += item.count; |
||||||
|
} else if (item.accuracy >= 0.6 && item.accuracy < 0.8) { |
||||||
|
countGroupedData[2] += item.count; |
||||||
|
} else if (item.accuracy >= 0.8) { |
||||||
|
countGroupedData[3] += item.count; |
||||||
|
} |
||||||
|
totalCount += item.count; |
||||||
|
}); |
||||||
|
|
||||||
|
// 计算每个区间的百分比
|
||||||
|
const groupedData = countGroupedData.map(count => totalCount > 0 ? count / totalCount : 0); |
||||||
|
|
||||||
|
return { |
||||||
|
tooltip: { |
||||||
|
trigger: 'axis', |
||||||
|
axisPointer: { |
||||||
|
type: 'shadow', |
||||||
|
}, |
||||||
|
// 修改 formatter 函数,将数值转换为百分比形式
|
||||||
|
formatter: (params: any) => { |
||||||
|
const xAxisValue = params[0].name; |
||||||
|
const yAxisValue = (params[0].value * 100).toFixed(2) + '%'; |
||||||
|
return `${xAxisValue}<br/>试题数量占比: ${yAxisValue}`; |
||||||
|
}, |
||||||
|
}, |
||||||
|
xAxis: { |
||||||
|
type: 'category', |
||||||
|
data: ['<10%', '10%-60%', '60%-80%', '80%-100%'], |
||||||
|
}, |
||||||
|
yAxis: { |
||||||
|
type: 'value', |
||||||
|
min: 0, |
||||||
|
max: 1, |
||||||
|
interval: 0.2, |
||||||
|
axisLabel: { |
||||||
|
formatter: (value: number) => `${value * 100}%`, |
||||||
|
}, |
||||||
|
name: '试题数量占比', |
||||||
|
nameLocation: 'top', |
||||||
|
nameRotate: 0, // 让名称水平显示
|
||||||
|
nameGap: 0, // 调整名称与坐标轴的间距
|
||||||
|
nameTextStyle: { |
||||||
|
padding: [0, 40, 100, 0] // 给名称上方增加一些内边距,使其更靠上
|
||||||
|
} |
||||||
|
}, |
||||||
|
series: [ |
||||||
|
{ |
||||||
|
data: groupedData, |
||||||
|
type: 'bar', |
||||||
|
}, |
||||||
|
], |
||||||
|
}; |
||||||
|
}; |
||||||
|
|
||||||
|
return ( |
||||||
|
<div style={{ padding: 20 }}> |
||||||
|
<ESBreadcrumbComponent url="/exam-statistics" previousText={"考试统计"} currentText={"试卷分析"} /> |
||||||
|
{paperData && ( |
||||||
|
<> |
||||||
|
{/* 第一行:试卷名称 */} |
||||||
|
<Row gutter={16}> |
||||||
|
<Col span={24}> |
||||||
|
<Title level={3}>试卷名称:{paperData.paperName}</Title> |
||||||
|
<Button onClick={()=>handleGoBack()}>返回</Button> |
||||||
|
</Col> |
||||||
|
</Row> |
||||||
|
{/* 第二行:QuestionComponent 标签 */} |
||||||
|
<Row gutter={16} style={{ marginTop: 20 }}> |
||||||
|
<Col span={24}> |
||||||
|
<QuestionComponent questions={tableData} /> |
||||||
|
</Col> |
||||||
|
</Row> |
||||||
|
{/* 第三行:柱状图 */} |
||||||
|
<Row gutter={16} style={{ marginTop: 20 }}> |
||||||
|
<Col span={24}> |
||||||
|
<h1>准确率分析</h1> |
||||||
|
<ReactECharts option={getBarChartOption(paperData.questionAccuracyData)} /> |
||||||
|
</Col> |
||||||
|
</Row> |
||||||
|
</> |
||||||
|
)} |
||||||
|
</div> |
||||||
|
); |
||||||
|
}; |
||||||
|
|
||||||
|
export default withRouter(ExamPaperAnalysisDetailPage); |
@ -0,0 +1,76 @@ |
|||||||
|
import React from 'react'; |
||||||
|
|
||||||
|
// 定义单条题目的 props 类型
|
||||||
|
export interface SingleQuestionProps { |
||||||
|
question_types: number; // 题目类型,1 为单选,2 为多选
|
||||||
|
question_content: string; // 题干
|
||||||
|
answer: string; // 答案
|
||||||
|
options: string; // 答题选项
|
||||||
|
question_number: number; // 题目编号
|
||||||
|
accuracyRate: number; // 准确率
|
||||||
|
} |
||||||
|
|
||||||
|
// 定义组件接收的参数类型,为单条题目 props 的数组
|
||||||
|
interface QuestionProps { |
||||||
|
questions: SingleQuestionProps[]; |
||||||
|
style?: React.CSSProperties; // 用于外部传入样式,控制组件范围大小
|
||||||
|
} |
||||||
|
|
||||||
|
const QuestionComponent: React.FC<QuestionProps> = ({ questions, style }) => { |
||||||
|
return ( |
||||||
|
<div className="question-container" style={{ ...style, overflow: 'auto' }}> |
||||||
|
<h1>试题详情</h1> |
||||||
|
{questions.map((question, index) => { |
||||||
|
const { question_types, question_content, answer, options, question_number, accuracyRate } = question; |
||||||
|
|
||||||
|
// 处理 answer 为空的情况
|
||||||
|
const answerArray = answer ? answer.split(',') : []; |
||||||
|
// 处理 options 为空的情况
|
||||||
|
const optionArray = options ? options.split(',') : []; |
||||||
|
|
||||||
|
// 根据题目类型判断是单选还是多选
|
||||||
|
const isSingleChoice = question_types === 1; |
||||||
|
|
||||||
|
// 生成选项字母列表(A, B, C...)
|
||||||
|
const optionLetters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; |
||||||
|
|
||||||
|
return ( |
||||||
|
<div key={index} className="single-question" style={{ background: "#B5DBFF", marginBottom: 20 }}> |
||||||
|
{/* 显示题目编号、题干和准确率 */} |
||||||
|
<p className="question-title"> |
||||||
|
{question_number}. {question_content} |
||||||
|
<span className="accuracy-rate">准确率:{(accuracyRate* 100).toFixed(2) + '%'}</span> |
||||||
|
</p> |
||||||
|
{/* 显示答题选项 */} |
||||||
|
<div className="option-list"> |
||||||
|
{optionArray.map((option, optionIndex) => { |
||||||
|
const letter = optionLetters[optionIndex]; |
||||||
|
const isChecked = answerArray.includes(letter); |
||||||
|
return ( |
||||||
|
<label key={optionIndex} className="option-label" style={{ marginRight: 20 }}> |
||||||
|
{isSingleChoice ? ( |
||||||
|
<input |
||||||
|
type="radio" |
||||||
|
checked={isChecked} |
||||||
|
disabled |
||||||
|
/> |
||||||
|
) : ( |
||||||
|
<input |
||||||
|
type="checkbox" |
||||||
|
checked={isChecked} |
||||||
|
disabled |
||||||
|
/> |
||||||
|
)} |
||||||
|
{option.replace(":", ".")} |
||||||
|
</label> |
||||||
|
); |
||||||
|
})} |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
); |
||||||
|
})} |
||||||
|
</div> |
||||||
|
); |
||||||
|
}; |
||||||
|
|
||||||
|
export default QuestionComponent; |
@ -0,0 +1,57 @@ |
|||||||
|
import React from 'react'; |
||||||
|
import QuestionComponent from './compoents/QuestionComponent'; |
||||||
|
import ExamPaperAnalysisDetailPage from "./compoents/ExamPaperAnalysisDetailPage"; |
||||||
|
|
||||||
|
const ExamPaperAnalysisDetail: React.FC = () => { |
||||||
|
const questions = [ |
||||||
|
{ |
||||||
|
question_types: 1, |
||||||
|
question_content: '一天有多少小时?', |
||||||
|
answer: 'B', |
||||||
|
options: 'A:12小时,B:24小时,C:36小时', |
||||||
|
question_number: 1, |
||||||
|
accuracyRate: '90%' |
||||||
|
}, |
||||||
|
{ |
||||||
|
question_types: 2, |
||||||
|
question_content: '以下哪些是水果?', |
||||||
|
answer: 'A,C', |
||||||
|
options: 'A:苹果,B:黄瓜,C:香蕉', |
||||||
|
question_number: 2, |
||||||
|
accuracyRate: '80%' |
||||||
|
}, |
||||||
|
{ |
||||||
|
question_types: 2, |
||||||
|
question_content: '以下哪些是水果?', |
||||||
|
answer: 'A,C', |
||||||
|
options: 'A:苹果,B:黄瓜,C:香蕉', |
||||||
|
question_number: 2, |
||||||
|
accuracyRate: '80%' |
||||||
|
}, |
||||||
|
{ |
||||||
|
question_types: 2, |
||||||
|
question_content: '以下哪些是水果?', |
||||||
|
answer: 'A,C', |
||||||
|
options: 'A:苹果,B:黄瓜,C:香蕉', |
||||||
|
question_number: 2, |
||||||
|
accuracyRate: '80%' |
||||||
|
}, |
||||||
|
{ |
||||||
|
question_types: 2, |
||||||
|
question_content: '以下哪些是水果?', |
||||||
|
answer: 'A,C', |
||||||
|
options: 'A:苹果,B:黄瓜,C:香蕉', |
||||||
|
question_number: 2, |
||||||
|
accuracyRate: '80%' |
||||||
|
} |
||||||
|
]; |
||||||
|
|
||||||
|
return ( |
||||||
|
<div> |
||||||
|
<ExamPaperAnalysisDetailPage |
||||||
|
/> |
||||||
|
</div> |
||||||
|
); |
||||||
|
}; |
||||||
|
|
||||||
|
export default ExamPaperAnalysisDetail; |
Loading…
Reference in new issue