用React和Laravel实现一个在线答题试卷模块,包括单选,多选,判断和填空题(下)

2 篇文章 0 订阅
2 篇文章 0 订阅

这篇是承接上一个博客的下篇,上一篇看用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文件保存。
考试结果页面

  • 27
    点赞
  • 37
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

阿黄勇闯天涯

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值