原文链接:https://www.qiuzhi99.com/articles/ruby/97610.html
最近大半年做了一个新项目,把 rails 7 新特性探究得差不多,学习了很多东西,也有一些经验可以分享出来。
首先要分享的是,我最近写了十几个 gem,本来打算私用的,现在改变想法,把它分享出来,希望对大家有所帮助。
除了 gem 可能还有其他经验分享,等我慢慢梳理更新。
源码:https://github.com/hfpp2012copy/where_streets (欢迎 star)
实现省市县镇多级联动功能,网络上关于 ruby 的 gem 好像是有,不过有些老了,不太好用。
我自己实现了一个,很简单。
我会把源码和使用方法分享出来。
大家可以一起学习讨论。
先看功能:
也可以是三级联动的,把镇去掉就行。
还有下面这种形式:
https://rubygems.org/gems/where_streets
第一步:安装
bundle add where_streets
第二步:直接在表单中用
例如:
<%= bootstrap_form_with model: admin_user, url: admin_account_path, method: :put, data: { controller: "form--request ts--cities-select dropzone-uploader" }, html: { class: "needs-validation mt-3" } do |form| %>
<%= form.fields_for :avatar do |f| %>
<%= f.avatar_file_field :upload_avatar, remove_path: remove_avatar_admin_users_path %>
<% end %>
<%= form.fields_for :admin_profile do |f| %>
<%= f.text_field :fullname %>
<%= f.rich_text_area :about, style: "height: 100px;" %>
<% end %>
<%= form.text_field :email, disabled: true %>
<%= form.select :province, WhereStreets.find_provinces, { include_blank: t("labels.please_select") }, data: { ts__cities_select_target: "province" } %>
<%= form.select :city, WhereStreets.find_cities(form.object.province || ""), { include_blank: t("labels.please_select") }, data: { ts__cities_select_target: "city" } %>
<%= form.select :county, WhereStreets.find_counties(form.object.province || "", form.object.city || ""), { include_blank: t("labels.please_select") }, data: { ts__cities_select_target: 'county' } %>
<%= form.select :town, WhereStreets.find_towns(form.object.province || "", form.object.city || "", form.object.county || ""), { include_blank: t("labels.please_select") }, data: { ts__cities_select_target: 'town' } %>
<%= form.update_button %>
<% end %>
这里我用了 stimulus。
我也分享出来:
主要是这个 ts--cities-select
controller
// app/javascript/controllers/ts/cities_select_controller.js
import { Controller } from "@hotwired/stimulus";
import { get } from "@rails/request.js";
import TomSelect from "tom-select";
// Connects to data-controller="ts--cities-select"
export default class extends Controller {
static targets = ["province", "city", "county", "town"];
initialize() {
["province", "city", "county", "town"].forEach((name) : {
Object.defineProperty(this, `${name}Value`, {
get: function () {
return this[`${name}Target`].value;
},
});
Object.defineProperty(
this,
`tomSelect${name[0].toUpperCase() + name.slice(1)}`,
{
get: function () {
if (!this[name])
this[name] = new TomSelect(this[`${name}Target`], {
plugins: ["clear_button"],
});
return this[name];
},
}
);
});
}
connect() {
this.tomSelectProvince;
this.tomSelectCity;
this.tomSelectCounty;
if (this.hasTownTarget) this.tomSelectTown;
this.provinceTarget.addEventListener("change", () : {
this.fetchData({
url: "/admin/cities/cities_filter",
query: { province: this.provinceValue },
selectTarget: this.tomSelectCity,
});
});
this.cityTarget.addEventListener("change", () : {
this.fetchData({
url: "/admin/cities/counties_filter",
query: { province: this.provinceValue, city: this.cityValue },
selectTarget: this.tomSelectCounty,
});
});
this.countyTarget.addEventListener("change", () : {
if (!this.hasTownTarget) return;
this.fetchData({
url: "/admin/cities/towns_filter",
query: {
province: this.provinceValue,
city: this.cityValue,
county: this.countyValue,
},
selectTarget: this.tomSelectTown,
});
});
}
async fetchData(options) {
const { url, query, selectTarget } = options;
const response = await get(url, {
query,
responseKind: "json",
});
if (response.ok) {
this.setOptionsData(await response.json, selectTarget);
} else {
console.log(response);
}
}
setOptionsData(items, selectTarget) {
selectTarget.clear();
selectTarget.clearOptions();
selectTarget.addOptions(items);
}
render_option(data, escape) {
if (data.sub)
return `
<div>
<div class="text">${escape(data.text)}</div>
<div class="sub">${escape(data.sub)}</div>
</div>`;
else return `<div>${escape(data.text)}</div>`;
}
}
这里有用到 tom-select
,可以用 yarn add tom-select
安装一下。
查看上面的 js 代码,可以看到,这里用到了 api 查询(比如 /admin/cities/towns_filter
),因为每次选择位置后,都会发请求去得到数据,比如选了某个省,会把这个省的所有城市数据得到。
我这里可以新建一个 controller 就搞定。
# frozen_string_literal: true
module Admin
class CitiesController < BaseController
# @route GET /admin/cities/cities_filter (admin_cities_cities_filter)
def cities_filter
@cities = WhereStreets.find_cities(params[:province])
render json: @cities.map { |city| { text: city, value: city } }
end
# @route GET /admin/cities/counties_filter (admin_cities_counties_filter)
def counties_filter
@counties = WhereStreets.find_counties(params[:province], params[:city])
render json: @counties.map { |county| { text: county, value: county } }
end
# @route GET /admin/cities/towns_filter (admin_cities_towns_filter)
def towns_filter
@towns = WhereStreets.find_towns(params[:province], params[:city], params[:county])
render json: @towns.map { |town| { text: town, value: town } }
end
end
end
最后你把在 model 里存几个字段,比如 province, city 等存到数据库为就好。
原理比较简单,主要就是读取网络上最新的 json 文件进行解析数据。
提供了一些查找功能,比如找一个省下的所有城市,一个城市下的所有区等等。
require "singleton"
require "forwardable"
require "fast_blank"
require "msgpack"
class WhereStreets
autoload :VERSION, "where_streets/version"
FILE = MessagePack.unpack(File.read(File.expand_path("../pcas.mp", __dir__))).freeze
include Singleton
class << self
extend Forwardable
def_delegators :instance, :find_provinces, :find_cities, :find_counties, :find_towns
end
def find_provinces
FILE.keys
end
def find_cities(province)
return [] if province.blank?
handle_error do
FILE[province.to_s].keys
end
end
def find_counties(province, city)
return [] if [province, city].any? { |i| i.blank? }
handle_error do
FILE[province.to_s][city.to_s].keys
end
end
def find_towns(province, city, county)
return [] if [province, city, county].any? { |i| i.blank? }
handle_error do
FILE[province.to_s][city.to_s][county.to_s]
end
end
private
def handle_error
yield
rescue StandardError : e
puts e.inspect
puts e.backtrace
[]
end
end
这些在源码里可以研究到,最新一版本是用了 msgpack 这个库,也可以不用它,之前没有用的,可以通过代码去查之前的版本。
里面还有一些测试代码,可以看看其用法:
require "test_helper"
class WhereStreetsTest < Minitest::Test
def test_that_it_has_a_version_number
refute_nil ::WhereStreets::VERSION
end
def test_provinces
assert_equal 31,
WhereStreets.find_provinces.length
end
def test_cities
assert_equal %w[广州市 韶关市 深圳市 珠海市 汕头市 佛山市 江门市 湛江市 茂名市 肇庆市 惠州市 梅州市 汕尾市 河源市 阳江市 清远市 东莞市 中山市 潮州市 揭阳市 云浮市],
WhereStreets.find_cities("广东省")
assert_equal ["市辖区"], WhereStreets.find_cities("上海市")
assert_empty WhereStreets.find_cities("")
end
def test_counties
assert_equal %w[罗湖区 福田区 南山区 宝安区 龙岗区 盐田区 龙华区 坪山区 光明区],
WhereStreets.find_counties("广东省", "深圳市")
assert_equal %w[黄浦区 徐汇区 长宁区 静安区 普陀区 虹口区 杨浦区 闵行区 宝山区 嘉定区 浦东新区 金山区 松江区 青浦区 奉贤区 崇明区],
WhereStreets.find_counties("上海市", "市辖区")
assert_empty WhereStreets.find_counties("", "")
# assert_empty WhereStreets.find_counties("广东省", "海丰市")
end
def test_towns
assert_equal %w[梅陇镇 小漠镇 鹅埠镇 赤石镇 鮜门镇 联安镇 陶河镇 赤坑镇 大湖镇 可塘镇 黄羌镇 平东镇 海城镇 公平镇 附城镇 城东镇],
WhereStreets.find_towns("广东省", "汕尾市", "海丰县")
assert_empty WhereStreets.find_towns("", "", "")
end
end
以后有位置数据需要更新,只要替换 json 文件就行,这样就能保证使用到最新的位置信息。
哪里找最新的位置信息呢?
我是在这里找的:https://github.com/modood/Administrative-divisions-of-China
补充一下:
我自己的项目关于 view 的用法被我封装了,可以用起来更简单:
只要一行代码。
<%= form.cities_select :location, town: true %>
要出现镇就把 town
设为 true
, 反之就没有。
之所有这么简单是因为进行了封装,而且用了 bootstrap_form
可以参考了解一下(仅供参考):
# config/initializer/bootstrap_form.rb
module BootstrapForm
class FormBuilder
def cities_select(field_name, town: false, **_options)
location_select = province_select + city_select + county_select
location_select += town_select if town
content_tag(:div, class: "mb-3 row", data: { controller: "ts--cities-select" }) do
concat label(field_name, class: label_col)
concat(content_tag(:div, class: control_col) do
content_tag(:div, location_select, class: "row")
end)
end
end
private
def province_select
content_tag(
:div,
select_without_bootstrap(
:province,
WhereStreets.find_provinces,
{ include_blank: t("labels.please_select") },
data: { ts__cities_select_target: "province" },
class: "form-control"
),
class: "col"
)
end
def city_select
content_tag(
:div,
select_without_bootstrap(
:city,
WhereStreets.find_cities(object.province || ""),
{ include_blank: t("labels.please_select") },
data: { ts__cities_select_target: "city" },
class: "form-control"
),
class: "col"
)
end
def county_select
content_tag(
:div,
select_without_bootstrap(
:county,
WhereStreets.find_counties(object.province || "", object.city || ""),
{ include_blank: t("labels.please_select") },
data: { ts__cities_select_target: "county" },
class: "form-control"
),
class: "col"
)
end
def town_select
content_tag(
:div,
select_without_bootstrap(
:town,
WhereStreets.find_towns(object.province || "", object.city || "", object.county || ""),
{ include_blank: t("labels.please_select") },
data: { ts__cities_select_target: "town" },
class: "form-control"
),
class: "col"
)
end
end
end
主要是这里 cities_select 这个方法。(具体自己看吧)
最近别忘了 follow 和 star(换了一个新的 github 号)
有问题可以一起交流,wechat: qiuzhi99pro
其它的等我慢慢更新。想学 ruby 和 rails 的可以看看我录制的教程哈:https://www.qiuzhi99.com/playlists/ruby.html