You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React中radio按钮键盘/鼠标选中时事件参数获取异常问题

问题分析

当前代码存在两个核心问题:

  • 同一个handleChange函数同时处理单选框的onChange事件和全局keydown事件,但两种事件的event对象结构完全不同:
    • onChange事件的event是InputEvent,e.target指向触发事件的单选框,包含value、name等关键属性;
    • keydown事件的event是KeyboardEvent,e.target可能是页面任意元素,无法直接获取单选框的value和name,导致keydown时无法正确更新答题状态。
  • 单选框的checked状态仅依赖keyCode状态,无法和实际选中值关联,且全局监听keydown会导致所有页面按键都触发逻辑,干扰其他操作。
解决方案

我们需要将键盘事件和鼠标事件的处理逻辑拆分,同时确保键盘操作能精准定位到当前显示的题目,并触发对应的单选框选中和状态更新。

步骤1:拆分事件处理函数

将原handleChange拆分为两个独立函数:

  • handleRadioChange:处理单选框的点击/选中事件,负责更新答题状态;
  • handleKeyDown:处理键盘事件,根据按键(A/B/C/D)找到当前题目对应的单选框,手动触发其选中状态并调用handleRadioChange。

步骤2:优化状态管理

  • 移除keyCode状态,改用answersSelected来控制单选框的checked状态,确保状态和实际选中值一致;
  • 修复index1的使用问题(原代码中var index1 = 0在map中会导致索引混乱,改用map的第二个参数作为题目索引)。

步骤3:精准绑定键盘事件

仅在当前题目容器上绑定键盘事件,避免全局监听的干扰,同时确保键盘操作只作用于当前显示的题目。

修改后的完整代码
import { useState, useEffect, useRef } from "react";
import { useLocation } from "react-router-dom";

import { englishQuestions } from "../questions/englishQ";

const English = () => {
  const { state } = useLocation();
  const { user } = state;

  // 答题相关状态
  const [input, setInputs] = useState<{ 
    questionNumber: string;
    question: string;
    answer: string;
    status: "correct" | "wrong";
    marks: string;
  }[]>([]);
  const [answersSelected, setAnswersSelected] = useState<Record<string, string>>({});
  const [selected, setSelected] = useState<Record<string, boolean>>({});
  const [submitted, setSubmitted] = useState(false);
  const [allSelected, setAllSelected] = useState(false);
  const [currentQuestionIndex, setCurrentQuestionIndex] = useState(1);

  const answers = englishQuestions.map(item => item.answer);
  const numbersRegex = /\d+/g;

  // 处理单选框选中事件(鼠标/键盘触发)
  const handleRadioChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const { value, name } = e.target;
    // 解析题目编号和索引
    const currentQuestionNumber = name.slice(name.indexOf("englishQ"), 10);
    const currentQuestion = name.slice(name.indexOf(":") + 2);
    const currentArrayIndex = parseInt(name.match(numbersRegex)![0]) - 1;

    // 更新答题记录
    const isCorrect = answers[currentArrayIndex] === value;
    setInputs(prev => {
      // 先移除该题已有的记录(避免重复添加)
      const filtered = prev.filter(item => item.questionNumber !== currentQuestionNumber.trim());
      return [
        ...filtered,
        {
          questionNumber: currentQuestionNumber.trim(),
          question,
          answer: value,
          status: isCorrect ? "correct" : "wrong",
          marks: isCorrect ? "2" : "0",
        }
      ];
    });
    setAnswersSelected(prev => ({ ...prev, [name]: value }));
    setSelected(prev => ({ ...prev, [currentArrayIndex + 1]: true }));

    // 检查是否所有题目都已作答
    const answeredCount = Object.values(selected).filter(Boolean).length + 1;
    setAllSelected(answeredCount >= 10);

    // 更新本地存储
    const selectedValues = Object.values(answersSelected).map(val => ({ theSelectedValue: val }));
    localStorage.setItem("theSelectedValue", JSON.stringify(selectedValues));
  };

  // 处理键盘按键事件
  const handleKeyDown = (e: KeyboardEvent) => {
    if (submitted) return; // 已提交则不响应键盘操作

    const key = e.code.toUpperCase();
    let targetOptionId = "";
    switch(key) {
      case "KEYA":
        targetOptionId = `q${currentQuestionIndex}-a`;
        break;
      case "KEYB":
        targetOptionId = `q${currentQuestionIndex}-b`;
        break;
      case "KEYC":
        targetOptionId = `q${currentQuestionIndex}-c`;
        break;
      case "KEYD":
        targetOptionId = `q${currentQuestionIndex}-d`;
        break;
      default:
        return; // 非A/B/C/D按键不处理
    }

    // 找到对应的单选框并触发选中
    const targetRadio = document.getElementById(targetOptionId) as HTMLInputElement;
    if (targetRadio && !targetRadio.disabled) {
      targetRadio.checked = true;
      handleRadioChange({ target: targetRadio } as React.ChangeEvent<HTMLInputElement>);
    }
  };

  const submit = (e: React.FormEvent) => {
    e.preventDefault();
    setSubmitted(true);
    localStorage.setItem("studentId", JSON.stringify(user.studentId));
    localStorage.setItem("englishSubmitted", "true");
    localStorage.setItem("englishAnswers", JSON.stringify(input));
  };

  // 绑定/解绑键盘事件
  useEffect(() => {
    document.addEventListener("keydown", handleKeyDown);
    return () => document.removeEventListener("keydown", handleKeyDown);
  }, [currentQuestionIndex, submitted, answersSelected]);

  return (
    <>
      <div className="bg-gray-200 mx-auto my-10 w-3/4 p-10 shadow-sm shadow-gray-600 rounded-lg">
        <form onSubmit={submit} className="mb-10">
          {englishQuestions.map((question, idx) => {
            const questionNumber = idx + 1;
            const isCurrent = questionNumber === currentQuestionIndex;
            return (
              <div
                key={questionNumber}
                className={isCurrent ? "" : "hidden"}
              >
                <h3>
                  <span className={submitted ? "text-gray-400" : "font-bold"}>
                    Q {questionNumber}:
                  </span>{" "}
                  {question.start}{" "}
                  <span className="underline">{question.theWord}</span>{" "}
                  {question.end}
                </h3>
                {Object.entries(question.options[0]).map(([optionKey, optionValue]) => {
                  const radioName = `englishQ${questionNumber} : ${question.start} ${question.theWord} ${question.end}`;
                  const radioId = `q${questionNumber}-${optionKey.toLowerCase()}`;
                  return (
                    <div className="mb-3" key={optionKey}>
                      <label htmlFor={radioId}>{optionKey}: </label>
                      <input
                        id={radioId}
                        type="radio"
                        name={radioName}
                        value={optionValue}
                        onChange={handleRadioChange}
                        disabled={submitted}
                        checked={answersSelected[radioName] === optionValue}
                      />
                      <label htmlFor={radioId}> {optionValue}</label>
                      <br />
                    </div>
                  );
                })}
              </div>
            );
          })}
          <div className="flex justify-center">
            <button
              type="submit"
              name="submit"
              className="btnExam p-3 mx-auto cursor-pointer"
              disabled={!allSelected} // 未答完所有题则禁用提交按钮
            >
              Submit
            </button>
          </div>
        </form>
      </div>
    </>
  );
};

export default English;
关键修改点说明
  1. 拆分事件处理:

    • handleRadioChange专注于处理单选框的选中逻辑,统一处理鼠标和键盘触发的选中操作;
    • handleKeyDown仅负责解析按键,找到对应的单选框并触发handleRadioChange,避免直接处理状态更新逻辑。
  2. 状态关联优化:

    • 单选框的checked状态绑定到answersSelected,确保选中状态和实际存储的答案一致,不再依赖不稳定的keyCode;
    • 修复input状态重复添加的问题,更新答题记录时先移除该题已有记录,避免数组冗余。
  3. 键盘事件精准化:

    • 根据当前显示的题目索引生成单选框ID,确保键盘操作只作用于当前题目;
    • 已提交状态下禁用键盘操作,避免无效交互。
  4. 代码健壮性提升:

    • 修复原代码中未定义的allSelected状态;
    • 使用map的索引替代全局index1,避免索引混乱;
    • 为单选框添加唯一ID,解决原代码中多个单选框ID重复的问题(原代码所有题目的A选项ID都是"a",会导致label关联错误)。

内容的提问来源于stack exchange,提问作者Misbahu Abubakar

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.31 18:05:23