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

Ruby on Rails Admin表单实现国家、州、城市联动下拉菜单求助

分步实现Rails后台三级联动下拉框(国家/州/城市)

1. 准备模型与关联

先创建三个模型并设置层级关联:

生成模型

运行终端命令生成模型文件:

rails generate model Country name:string
rails generate model State name:string country:references
rails generate model City name:string state:references

执行数据库迁移:

rails db:migrate

补充模型关联代码

修改 app/models/country.rb:

class Country < ApplicationRecord
  has_many :states, dependent: :destroy
end

修改 app/models/state.rb:

class State < ApplicationRecord
  belongs_to :country
  has_many :cities, dependent: :destroy
end

修改 app/models/city.rb:

class City < ApplicationRecord
  belongs_to :state
end

2. 导入测试数据(可选)

在 db/seeds.rb 中添加测试数据方便调试:

# 创建国家
usa = Country.create(name: "美国")
china = Country.create(name: "中国")

# 创建对应州/省
california = usa.states.create(name: "加利福尼亚州")
guangdong = china.states.create(name: "广东省")

# 创建对应城市
california.cities.create(name: "洛杉矶")
guangdong.cities.create(name: "广州")

运行命令导入数据:

rails db:seed

3. 配置后台控制器

假设你在后台管理Address资源,修改后台控制器添加AJAX处理动作:

添加控制器动作

编辑 app/controllers/admin/addresses_controller.rb:

class Admin::AddressesController < ApplicationController
  # 其他默认动作...

  # 根据国家ID获取对应州
  def get_states
    @country = Country.find(params[:country_id])
    @states = @country.states.order(:name)
    respond_to do |format|
      format.turbo_stream
    end
  end

  # 根据州ID获取对应城市
  def get_cities
    @state = State.find(params[:state_id])
    @cities = @state.cities.order(:name)
    respond_to do |format|
      format.turbo_stream
    end
  end
end

更新路由配置

在 config/routes.rb 中添加后台路由:

namespace :admin do
  resources :addresses do
    collection do
      get :get_states
      get :get_cities
    end
  end
end

4. 编写后台表单视图

在后台表单视图(如 app/views/admin/addresses/_form.html.erb)中添加三个下拉框:

<div class="form-group">
  <%= label_tag :country_id, "国家" %>
  <%= select_tag :country_id, options_from_collection_for_select(Country.all.order(:name), :id, :name, @address.country_id), 
                  class: "form-control",
                  data: { turbo_controller: "dropdown", turbo_target: "states" } %>
</div>

<div class="form-group">
  <%= label_tag :state_id, "州/省" %>
  <div id="states">
    <%= render "states_dropdown", state_id: @address.state_id %>
  </div>
</div>

<div class="form-group">
  <%= label_tag :city_id, "城市" %>
  <div id="cities">
    <%= render "cities_dropdown", city_id: @address.city_id %>
  </div>
</div>

创建下拉框局部视图

创建 app/views/admin/addresses/_states_dropdown.html.erb:

<%= select_tag :state_id, options_from_collection_for_select(@states || [], :id, :name, state_id), 
                class: "form-control",
                disabled: @states.nil? || @states.empty?,
                data: { turbo_controller: "dropdown", turbo_target: "cities" } %>

创建 app/views/admin/addresses/_cities_dropdown.html.erb:

<%= select_tag :city_id, options_from_collection_for_select(@cities || [], :id, :name, city_id), 
                class: "form-control",
                disabled: @cities.nil? || @cities.empty? %>

创建Turbo Stream视图

创建 app/views/admin/addresses/get_states.turbo_stream.erb:

<%= turbo_stream.replace "states" do %>
  <%= render "states_dropdown", state_id: nil %>
<% end %>

创建 app/views/admin/addresses/get_cities.turbo_stream.erb:

<%= turbo_stream.replace "cities" do %>
  <%= render "cities_dropdown", city_id: nil %>
<% end %>

5. 编写联动逻辑的Turbo控制器

创建 app/javascript/controllers/dropdown_controller.js:

import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  connect() {
    this.element.addEventListener("change", this.handleChange.bind(this))
  }

  handleChange(event) {
    const selectedId = event.target.value
    const targetId = this.data.get("target")
    const targetElement = document.getElementById(targetId)

    if (!selectedId) {
      // 未选中值时清空目标下拉框
      targetElement.innerHTML = `<select class="form-control" disabled></select>`
      return
    }

    // 构造请求URL
    let url
    if (this.element.id === "country_id") {
      url = `/admin/addresses/get_states?country_id=${selectedId}`
    } else if (this.element.id === "state_id") {
      url = `/admin/addresses/get_cities?state_id=${selectedId}`
    }

    // 发送Turbo请求更新下拉框
    fetch(url, { headers: { "Accept": "text/vnd.turbo-stream.html" } })
  }
}

在 app/javascript/controllers/index.js 中注册控制器:

import DropdownController from "./dropdown_controller"
application.register("dropdown", DropdownController)

6. 保存关联数据(可选)

如果你的Address模型需要关联这三个层级,先修改模型:

class Address < ApplicationRecord
  belongs_to :country, optional: true
  belongs_to :state, optional: true
  belongs_to :city, optional: true
end

然后在控制器中允许参数:

def address_params
  params.require(:address).permit(:country_id, :state_id, :city_id, ...) # 其他业务参数
end

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.19 11:20:28