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

基于Rails 6与Active Admin的罚单违规关联表单设计咨询

解决方案:在Active Admin中构建带层级关联的多违规记录罚单表单

我刚好处理过类似的嵌套层级表单场景,结合你用Rails 6+Active Admin(基于Formtastic)的需求,给你一步步拆解实现方案:

1. 完善模型关联与验证

首先需要让Fine支持嵌套表单提交,同时保证层级关联的数据合法性:

修改 app/models/fine.rb

class Fine < ApplicationRecord
  has_many :violations, dependent: :destroy
  # 允许嵌套提交违规记录,自动过滤空记录,支持删除
  accepts_nested_attributes_for :violations, allow_destroy: true, reject_if: :all_blank
  
  # 原有关联保持不变
  has_many :articles, through: :violations
  has_many :letters, through: :violations
  has_many :commas, through: :violations
end

增强 app/models/violation.rb 的数据验证

避免前端联动失效时出现非法关联:

class Violation < ApplicationRecord
  belongs_to :fine
  belongs_to :article
  belongs_to :letter, optional: true
  belongs_to :comma, optional: true

  # 强制法规条目必填
  validates :article, presence: true
  # 验证款目必须属于选中的法规条目
  validate :letter_belongs_to_associated_article, if: -> { letter.present? }
  # 验证细则必须属于选中的款目
  validate :comma_belongs_to_associated_letter, if: -> { comma.present? }

  private

  def letter_belongs_to_associated_article
    errors.add(:letter, "必须属于已选中的法规条目") unless letter.article_id == article_id
  end

  def comma_belongs_to_associated_letter
    errors.add(:comma, "必须属于已选中的款目") unless comma.letter_id == letter_id
  end
end

2. 配置Active Admin的罚单表单

在Fine的Admin资源里配置嵌套表单,支持添加/删除多条违规记录,并初始化层级下拉框:

修改 app/admin/fine.rb

ActiveAdmin.register Fine do
  # 允许提交的参数,必须包含违规记录的嵌套属性
  permit_params :fine_number, :issued_at, # 替换为你的罚单其他字段
                violations_attributes: [:id, :article_id, :letter_id, :comma_id, :note, :_destroy]

  form do |f|
    f.inputs "罚单基本信息" do
      f.input :fine_number, label: "罚单编号"
      f.input :issued_at, label: "开具日期"
      # 这里添加你的罚单其他字段
    end

    # 嵌套违规记录表单
    f.inputs "违规记录" do
      f.has_many :violations, 
                 heading: false, 
                 allow_destroy: true, 
                 new_record: "添加一条违规记录" do |v_f|
        # 法规条目下拉框
        v_f.input :article, 
                  label: "法规条目", 
                  collection: Article.all.order(:number), 
                  include_blank: false
        # 款目下拉框(初始为空,通过JS动态加载)
        v_f.input :letter, 
                  label: "款目", 
                  collection: -> { v_f.object.article.present? ? v_f.object.article.letters.order(:number) : [] }, 
                  include_blank: true
        # 细则下拉框(初始为空,通过JS动态加载)
        v_f.input :comma, 
                  label: "细则", 
                  collection: -> { v_f.object.letter.present? ? v_f.object.letter.commas.order(:number) : [] }, 
                  include_blank: true
        # 违规备注
        v_f.input :note, label: "违规备注"
        # 删除按钮
        v_f.input :_destroy, as: :boolean, label: "删除这条违规记录"
      end
    end

    f.actions
  end

  # 添加JSON接口,用于动态加载款目和细则
  controller do
    def letters
      @article = Article.find(params[:article_id])
      render json: @article.letters.pluck(:id, "#{:number} - #{:content}")
    end

    def commas
      @letter = Letter.find(params[:letter_id])
      render json: @letter.commas.pluck(:id, "#{:number} - #{:content}")
    end
  end

  # 注册接口路由
  member do
    get :letters
    get :commas
  end
end

3. 实现层级下拉的动态联动

添加JavaScript来实现「选法规→加载款目」「选款目→加载细则」的联动效果:

创建 app/assets/javascripts/admin/violation_form.js

document.addEventListener('turbolinks:load', function() {
  // 给已存在的违规记录绑定联动事件
  bindViolationFormEvents();

  // 监听"添加违规记录"按钮,给新生成的表单绑定事件
  document.querySelector('.has_many_add')?.addEventListener('click', function() {
    setTimeout(() => bindViolationFormEvents(), 100);
  });
});

function bindViolationFormEvents() {
  document.querySelectorAll('.violations .inputs').forEach(container => {
    const articleSelect = container.querySelector('.input.article select');
    const letterSelect = container.querySelector('.input.letter select');
    const commaSelect = container.querySelector('.input.comma select');

    // 避免重复绑定事件
    if (articleSelect.dataset.bound === 'true') return;
    articleSelect.dataset.bound = 'true';

    // 法规条目变化时,加载对应的款目
    articleSelect.addEventListener('change', function() {
      const articleId = this.value;
      if (!articleId) {
        letterSelect.innerHTML = '<option value="">请选择款目</option>';
        commaSelect.innerHTML = '<option value="">请选择细则</option>';
        return;
      }

      fetch(`/admin/fines/${document.querySelector('.resource_id').value}/letters?article_id=${articleId}`)
        .then(res => res.json())
        .then(letters => {
          let options = '<option value="">请选择款目</option>';
          letters.forEach(([id, text]) => options += `<option value="${id}">${text}</option>`);
          letterSelect.innerHTML = options;
          commaSelect.innerHTML = '<option value="">请选择细则</option>';
        });
    });

    // 款目变化时,加载对应的细则
    letterSelect.addEventListener('change', function() {
      const letterId = this.value;
      if (!letterId) {
        commaSelect.innerHTML = '<option value="">请选择细则</option>';
        return;
      }

      fetch(`/admin/fines/${document.querySelector('.resource_id').value}/commas?letter_id=${letterId}`)
        .then(res => res.json())
        .then(commas => {
          let options = '<option value="">请选择细则</option>';
          commas.forEach(([id, text]) => options += `<option value="${id}">${text}</option>`);
          commaSelect.innerHTML = options;
        });
    });

    // 编辑罚单时,初始化已选中的层级数据
    if (articleSelect.value) articleSelect.dispatchEvent(new Event('change'));
    if (letterSelect.value) letterSelect.dispatchEvent(new Event('change'));
  });
}

别忘了在Active Admin的布局中引入这个JS文件,或者直接添加到app/assets/javascripts/active_admin.js里。

4. 测试与优化

  • 创建新罚单时,点击「添加一条违规记录」,选择法规条目后,款目下拉框会自动加载对应选项,选款目后细则同理
  • 编辑已有罚单时,会自动初始化已关联的层级数据
  • 可以根据实际需求调整验证规则,比如是否强制要求选择款目或细则
  • 若性能要求高,可以将法规-款目-细则的预数据渲染到页面的data属性中,减少AJAX请求

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.05.11 08:05:22