这篇是承接上一个博客的下篇,上一篇看用React和Laravel实现一个在线答题试卷模块,包括单选,多选,判断和填空题(上)
三、业务实现
1. 数据库设计
详细内容看上一篇用React和Laravel实现一个在线答题试卷模块,包括单选,多选,判断和填空题(上)
2. 后端业务
后端业务总体来说比较简单,没有过多的数据量或者考虑并发,线程安全,数据安全等。
a. 获取考试题目。就是一个简单的get方法获取所有的题目
public function getQuestions() {
$questionList = DB::table('questions')
->get()
->all();
return response()->json([
"code" => 200,
"msg" => "获取题目成功",
"data" => $questionList
]);
}
b. 保存考试结果。创建一个基本的后端模型,有控制器,路由等。在我的Laravel中创建了一个控制器ExamController,ExamController里面saveTheoryResult方法保存考试数据。保存用户考试数据分两张情况,1. 用户第一次考核 2. 用户不是第一次考核。如果用户第一次进行考试,则添加数据;如果用户不是第一次考试,则修改考试数据。具体代码实现如下:
class ExamController extends Controller
{
public function saveTheoryResult(Request $request)
{
$data = $request->all();
// 1. 用户第一次进行考核
// 2. 用户已经完成了考核
try {
$userId = DB::table("theory_exam")
->where("user_id", $data['userId'])
->select("user_id")
->first();
// 用户第一次考核
if (!$userId) {
$result = DB::table('theory_exam')
->insert([
"user_id" => $data['userId'],
"answer" => json_encode($data['answer']),
"score" => $data['score'],
"theory_time" => $data['theoryTime']
]);
return response()->json([
"code" => 201,
"msg" => "保存理论考核成功",
"data" => $data
]);
} // 用户已经考过
else {
$result = DB::table('theory_exam')
->where("user_id", $data['userId'])
->update([
"answer" => json_encode($data['answer']),
"score" => $data['score'],
"theory_time" => $data['theoryTime']
]);
return response()->json([
"code" => 200,
"msg" => "修改理论考核成功",
"data" => $data
]);
}
} catch (Exception $exception) {
return response()->json([
"code" => $exception->getCode(),
"msg" => "保存理论考核失败",
"error" => $exception->getMessage()
]);
}
}
}
c. 写完控制器然后就需要将控制器中的方法放在定义的路由中。
Route::get('api/questions', 'QuestionController@getQuestions');
Route::post('/api/exam/practice', 'ExamController@savePracticeResult');
Route::post('/api/exam/theory', 'ExamController@saveTheoryResult');
3. 前端业务
理论考核部分主要包含一个试卷考试功能,计时器功能和提交试卷并且导出考试结果功能。考试试卷是从数据库中通过后端发送到前端的(也可以直接使用一个json数据源文件保存考试题目)。
最后的显示的样子
首先我创建需要的state来保存考试数据,这里单选题和判断题都的正确答案放在一起,多选题的正确答案放在另一个state。
this.state = {
radioValues: [], // 用户的单选题答案
checkBoxValues: [], // 用户多选题答案
textAreaValues: [], // 用户问答题答案
radioCorrect: [], // 单选题和判断题正确答案
checkBoxCorrect: [], // 多选题正确答案
finalScore: 0, // 最终分数
time: 3600, //1小时计时器
questionList: [], //题目
};
然后我们要请求后端发送考试题目数据。这里根据不同的type来分辨题目的类型,以便储存在不同的状态state中(具体数据量设计请看上一篇)。
axios
.get("/questions")
.then((res) => {
this.setState(
{
questionList: res.data,
},
// setState回调函数,会在setState异步更新后执行
() => {
this.setState({
radioCorrect: this.state.questionList
.filter((item) => item.type == "dx" || item.type == "pd")
.map((item) => {
return { name: item.name, correct: item.correct };
}),
// .flat(2),
checkBoxCorrect: this.state.questionList
.filter((item) => item.type == "ddx")
.map((item) => {
return { name: item.name, correct: item.correct.split('') };
}),
// .flat(2),
}, () => {
console.log("radioCorrect: ", this.state.radioCorrect);
console.log("checkBoxCorrect: ", this.state.checkBoxCorrect);
});
}
);
console.log(res);
})
.catch((err) => console.log(err));
接着我们需要创建一个form标签来包括所有的题目,因为本质上试卷也就是表单form。在form中,我们实现一个一个不同类型的题目。
a. 单选题实现
单选题是最简单的题型,用户在提供的选项中选择一个答案即可。
//单选题目
<div className="topicLeftConCon">
{/* 单选题 */}
{questionList
.filter((item) => item.type == "dx")
.map((item, index) => (
<div className="topicLeftConList" key={item.id}>
<p className="topicLeftConListQ">
{item.id}. {item.title} (X分)
</p>
<Radio.Group
onChange={this.handleRadioChange}
name={item.name}
>
{JSON.parse(item.options).map((b) => (
<p className="topicLeftConListA" key={b.value}>
<Radio value={b.value}>
<span className="topicLeftConListASpan">
{b.name}
</span>
</Radio>
</p>
))}
</Radio.Group>
</div>
))}
</div>
这里我们使用Antd的Radio和RadioGroup创建多个单选题目录,每个RadioGroup有一个handleRadioChange的方法。这个方法就是简单的选中相应的radio选项然后更新state,将选中的选项保存在state中。
// handleRadioChange
handleRadioChange = (event) => {
// console.log(event);
const { radioValues } = this.state;
const { name, value } = event.target;
const newValues = radioValues.filter((ele, index) => ele.name != name);
this.setState({
radioValues: [...newValues, { name, value }],
});
};
b. 判断题实现
判断题要求用户判断一个陈述是正确还是错误。判断题的实现和单选题一模一样,只是选项变成了两个,这里就不再过多叙述了。
c. 多选题实现
多选题允许用户选择多个答案。与单选题类似,但是要使用不同的数据结构和考虑不同的方法。多选题题目是一个数组,每个数组中的元素有包含一个对象name和values,values也是一个数组,values包含正确的选项。
<div>
<p className="topicLeftConTitle">
{" "}
3. 多选题(共3小题,总分: 30分)
</p>
<div className="topicLeftConCon">
{/* 多选题 */}
{questionList
.filter((item) => item.type == "ddx")
.map((item, index) => (
<div className="topicLeftConList" key={item.id}>
<p className="topicLeftConListQ">
{item.id}. {item.title} (X分)
</p>
<CheckboxGroup name={item.name}>
{JSON.parse(item.options).map((b) => (
<p key={b.value}>
<Checkbox
value={b.value}
onChange={this.handleCheckBoxChange}
>
<span>{b.name}</span>
</Checkbox>
</p>
))}
</CheckboxGroup>
</div>
))}
</div>
这里使用Antd的CheckboxGroup和Checkbox标签,每个CheckboxGroup有一个onChange方法handleCheckBoxChange。注意这里需要先判断是否为第一次选择这个多选题,如果是第一次直接添加选项到state checkBoxValues中,如果不是第一次选择,在更新state状态时先要读取之前的状态,然后判断checkbox是勾选还是勾销,对结果进行增加或删除。(这里踩了很多坑)
handleCheckBoxChange = (e) => {
console.log(e);
const { checkBoxValues } = this.state;
const { name, value, checked } = e.target;
const index = checkBoxValues.findIndex((item) => item.name === name);
if (index === -1) {
this.setState({
checkBoxValues: [...checkBoxValues, { name, values: [...value] }],
});
} else {
this.setState({
checkBoxValues: [
...checkBoxValues.filter((item, i) => i != index),
{
name,
values: checked
? [...checkBoxValues[index].values, value]
: [...checkBoxValues[index].values.filter((e, i) => e != value)],
},
],
});
}
};
d. 问答题实现
问答题要求用户通过题目写一段自己的答案,问答题没有标准答案,所有在数据中没有正确的答案。总体实现而言和单选和判断题差不多,这里使用Antd的Input.TextArea。然后每个问答题有一个相应的onChange处理函数handleTextAreaChange。
<div>
<p className="topicLeftConTitle">
{" "}
4. 问答题(共2小题,总分: 40分)
</p>
<div className="topicLeftConCon">
{/* 多选题 */}
{questionList
.filter((item) => item.type == "wd")
.map((item, index) => (
<div className="topicLeftConList" key={item.id}>
<p className="topicLeftConListQ">
{item.id}. {item.title} (X分)
</p>
<Input.TextArea
style={{ width: "500px", height: "150px" }}
onChange={this.handleTextAreaChange}
// value={this.state.textAreaValue}
name={item.name}
>
回答
</Input.TextArea>
</div>
))}
</div>
</div>
e. 提交最后结果
当用户完成了考试,form标签的handleSubmit方法就会执行,进行结算用户的分数并且保存用户考试的数据到数据库中 。其中需要注意的一点是多选题由于values结果是一个数组,所有比较数组是否相等不能直接用或者=比较,需要用到arraysAreEqual这个方法来比较数组的值是否相等。
handleSubmit = (event) => {
event.preventDefault();
const {
radioValues,
checkBoxValues,
textAreaValues,
radioCorrect,
checkBoxCorrect,
User,
time,
} = this.state;
// 对比用户答案和正确答案,得到最终分数
let score = 0;
console.log("radioCorrect: ",radioCorrect)
console.log("radioValues: ",radioValues)
radioCorrect.forEach((item, index) => {
if (item.correct == radioValues[index].value) {
score++;
}
});
checkBoxCorrect.forEach((item, index) => {
if (this.arraysAreEqual(item.correct, checkBoxValues[index].values)) {
// debugger
score++;
}
});
console.log("分数:" + score);
this.setState({
finalScore: score,
});
//合并所有题答案
const finalAnswer = [...radioValues, ...checkBoxValues, ...textAreaValues];
setTimeout(() => {
// 将选择题和问答题合并,保存用户答案和最终分数
axios
.post("/exam/theory", {
userId,
answer,
score,
theoryTime: time,
})
.then((response) => {
console.log(response);
})
.catch((error) => {
console.error(error);
});
}, 1000);
this.setState({
modalVisible: false,
});
var path = {
pathname: "/examresult",
state: {
account: this.state.User.account,
userId: this.state.User.id,
score: score,
answer: finalAnswer,
},
};
// 跳转到考试结果页面
history.push(path);
};
最后在考试结果页面会显示考生信息,考试分数以及答题的结果,并且可以将这个数据导出为pdf文件保存。