Rails 6嵌套表单开发测验应用逐题提交实现方案咨询
你选的嵌套表单+最终统一提交的方案完全可行,比逐题调用接口存临时答案的实现成本更低、逻辑更顺,适合这个答题场景。你现在跑不通的核心原因是几个代码逻辑缺口,按下面步骤改就能实现效果。
现有代码的核心问题
new动作中仅构建了1个quiz_answer实例,10道题需要对应10个独立的answer实例才能存储所有答案,当前代码的fields_for只会渲染1组答案输入框- 局部视图遍历对象错误:
@quiz是初始化的空Quiz实例,不是题目集合,和嵌套表单的结构完全不匹配 - 缺少单题显示、切换的前端交互逻辑,默认会把所有题目一次性渲染出来
- 缺少嵌套属性对应的强参数配置,提交时答案参数会被Rails默认拦截
具体修改步骤
1. 调整控制器逻辑
首先修改QuizzesController,给每道激活的题目预构建对应答案实例,补上强参数配置:
class QuizzesController < ApplicationController def new @quiz = Quiz.new # 查询所有启用的题目,可根据需要调整排序规则 @active_questions = QuizQuestion.where(active: true).order(:id) # 为每道题预构建空的关联答案,绑定对应题目ID @active_questions.each do |question| @quiz.quiz_answers.build(quiz_question_id: question.id) end end def create @quiz = Quiz.new(quiz_params) # 绑定当前答题用户ID,若使用Devise认证可直接取current_user @quiz.user_id = current_user.id respond_to do |format| if @quiz.save # 可在此处或模型回调中添加判分逻辑,对照选项的correct字段计算得分 format.html { redirect_to quiz_url(@quiz), notice: "测验提交成功" } format.json { render :show, status: :created, location: @quiz } else # 渲染错误页时重新加载题目列表 @active_questions = QuizQuestion.where(active: true).order(:id) format.html { render :new, status: :unprocessable_entity } format.json { render json: @quiz.errors, status: :unprocessable_entity } end end end private def quiz_params params.require(:quiz).permit( :user_id, quiz_answers_attributes: [:id, :quiz_question_id, :quiz_option_id, :answer, :_destroy] ) end end
2. 重构表单视图
原来的局部视图可以直接废弃,把答题逻辑整合到主表单中,默认仅显示第一题,其余题目隐藏:
<%# app/views/quizzes/new.html.erb %> <%= form_with(model: @quiz) do |form| %> <div class="quiz-container"> <% @active_questions.each_with_index do |question, index| %> <%# 按索引匹配预构建的answer表单构造器 %> <% answer_form = form.fields_for(:quiz_answers)[index] %> <div class="question-item" data-index="<%= index %>" style="<%= index == 0 ? '' : 'display: none;' %>"> <h3><%= question.question %></h3> <%# 隐式传入题目ID,绑定答案和题目的关联关系 %> <%= answer_form.hidden_field :quiz_question_id, value: question.id %> <% question.quiz_options.each do |option| %> <div class="form-check mb-2"> <%= answer_form.radio_button :quiz_option_id, option.id, class: 'form-check-input', required: true %> <%= answer_form.label :quiz_option_id, option.answer, value: option.id, class: 'form-check-label' %> </div> <% end %> <div class="mt-4"> <% if index == @active_questions.size - 1 %> <%# 最后一题显示提交按钮 %> <%= form.submit "提交测验", class: 'btn btn-primary' %> <% else %> <%# 非最后一题显示下一题按钮 %> <button type="button" class="btn btn-primary next-question" data-target="<%= index + 1 %>">下一题</button> <% end %> <%# 可选:上一题返回按钮 %> <% if index > 0 %> <button type="button" class="btn btn-secondary prev-question" data-target="<%= index - 1 %>">上一题</button> <% end %> </div> </div> <% end %> </div> <% end %>
3. 添加前端切换逻辑
用原生JS实现题目的显示/隐藏切换,加选项非空校验,不需要引入额外前端框架。如果是Rails 6默认的Webpacker环境,可以把代码写到app/javascript/packs/quizzes.js中,在对应页面引入;也可以直接写在页面底部的<script>标签里:
document.addEventListener('DOMContentLoaded', () => { // 下一题逻辑 document.querySelectorAll('.next-question').forEach(btn => { btn.addEventListener('click', (e) => { const currentBlock = e.target.closest('.question-item') const targetIndex = e.target.dataset.target // 校验当前题是否已选答案 const selected = currentBlock.querySelector('input[type="radio"]:checked') if (!selected) { alert('请先选择答案再进入下一题') return } // 切换题目显示 currentBlock.style.display = 'none' document.querySelector(`.question-item[data-index="${targetIndex}"]`).style.display = 'block' // 可选:滚动到页面顶部 window.scrollTo(0, 0) }) }) // 上一题逻辑(可选) document.querySelectorAll('.prev-question').forEach(btn => { btn.addEventListener('click', (e) => { const currentBlock = e.target.closest('.question-item') const targetIndex = e.target.dataset.target currentBlock.style.display = 'none' document.querySelector(`.question-item[data-index="${targetIndex}"]`).style.display = 'block' window.scrollTo(0, 0) }) }) })
可选优化项
- 可添加答题进度条,展示当前答题进度,提升用户体验
- 判分逻辑建议放到Quiz模型的回调中实现,自动对照选项的
correct字段标记答案对错、计算得分,避免控制器逻辑过重 - 可将用户已选答案临时存在localStorage中,防止页面刷新导致答案丢失
- 提交前可增加答案确认页,展示用户所有选择供核对后再提交
内容的提问来源于stack exchange,提问作者yellowskull
相关产品推荐
相关产品推荐

