三级联动就是选了省自动出市,选了市自动出区,下拉框联动。
核心三步:
① 选省级——拿到选中的省
② 过滤市级——从数据里找这个省下的市
③ 渲染市级下拉——同理渲染区级
坑:切换省要清空市和区、option value 是 ID、用 find/filter 过滤数据。
注意最后一步,这是新手最容易漏的:上级一变,下级全部清空。如果你只重新生成了城市、忘了清空区县,页面上就会残留上一个省份的旧区县,甚至反复切换后出现一大堆重复选项。
// 一份写死在前端的省市区数据:一层套一层
const chinaData = {
// 先按省份名一层
"河南省": {
// 省里套着它的城市
cities: {
// 每个城市里套着它的区县数组
"郑州市": ["金水区", "中原区", "二七区", "惠济区"],
"洛阳市": ["涧西区", "西工区", "老城区", "洛龙区"]
}
},
"山东省": {
cities: {
"济南市": ["历下区", "市中区", "槐荫区", "历城区"],
"青岛市": ["市南区", "市北区", "黄岛区", "崂山区"]
}
},
"广东省": {
cities: {
"广州市": ["越秀区", "海珠区", "荔湾区", "天河区"],
"深圳市": ["福田区", "罗湖区", "南山区", "宝安区"]
}
}
};
// 取值例子:chinaData["河南省"].cities["郑州市"] → 直接拿到那 4 个区
这是"一层套一层"的对象:省 → 市里套着城市,城市 → 里套着区县数组。取值非常直观:chinaData["河南省"].cities["郑州市"] 直接就是金水区那一串。缺点是没法直接传给后端(后端不认这种嵌套),只适合小数据、写死在前端的场景。
// 一份扁平数据:所有省市县平铺在一个数组里
const regions = [
// 省 → parentId 是 0 表示最顶层
{ id: 1, name: "河南省", parentId: 0, level: 1 },
{ id: 2, name: "山东省", parentId: 0, level: 1 },
// 市:parentId 指向省的 id
{ id: 11, name: "郑州市", parentId: 1, level: 2 },
{ id: 12, name: "洛阳市", parentId: 1, level: 2 },
// 区:parentId 指向市的 id
{ id: 111, name: "金水区", parentId: 11, level: 3 },
{ id: 112, name: "中原区", parentId: 11, level: 3 }
];
// 查找子级:凡是 parentId 等于选中项 id 的,就是它的子级 → 谁的"爸"是它,谁就是它孩子
// 把省 id 当爹,挑出所有归它管的市(普通 function)
const cities = regions.filter(function (r) {
return r.parentId === selectedProvinceId;
});
| 对比 | 对象嵌套 | 扁平数组 + parentId |
|---|---|---|
| 可读性 | 一眼看清层级 | 要靠 parentId 自己理关系 |
| 查找效率 | 直接对象取值,快 | 要遍历 filter 找 |
| 数据量 | 只适合小数据 | 能撑大数据 |
| 对接后端 | 没法直接传 | 标准接口格式 |
parentId 的思路是:每个元素记一下"我爸是谁的 id",查子级就 filter 谁的 parentId 等于选中项的 id。真实项目里后端返回的基本都是这种扁平格式。
// 1. 初始化:把所有省填进第一个下拉框 → 页面一打开,省份先列出来
// 把 chinaData 里的省名取出来当数组
const provinces = Object.keys(chinaData);
// 三个下拉框都先拿到
const $province = $("#province"),
$city = $("#city"),
$district = $("#district");
// 每个省造一个 option 塞进省下拉框(普通 functi
provinces.forEach(function (p) {
$province.append('<option value="' + p + '">' + p + '</option>');
});
// 2. 省份 change → 更新城市(同时清空区县)→ 选了省,市和区要跟着变
$province.on("change", function () {
// 取出现在选的是哪个省
const selectedProvince = $(this).val();
// ★ 先清空城市 → 把旧城市选项全丢掉
$city.html('<option value="">请选择城市</option>');
// ★ 旧区县失效,一并清空 → 市都换了,区更不能留
$district.html('<option value="">请选择区县</option>');
// 判空,没选就停 → 选回"请选择"就到此为止
if (!selectedProvince) return;
// 从数据里查出这个省有哪些市
const cities = Object.keys(chinaData[selectedProvince].cities);
// 每个市造一个 option 填进市下拉框(普通 functi
cities.forEach(function (city) {
$city.append('<option value="' + city + '">' + city + '</option>');
});
});
// 3. 城市 change → 更新区县(和上面是同一个模板)→ 选了市,区跟着变
$city.on("change", function () {
// 取现在选的省
const selectedProvince = $province.val();
// 取现在选的市
const selectedCity = $(this).val();
// 先清空区县
$district.html('<option value="">请选择区县</option>');
// 省或市没选就停
if (!selectedProvince || !selectedCity) return;
// ★ || [] 兜底 → 没录这个市的区时,给个空数组别报错
const districts = chinaData[selectedProvince].cities[selectedCity] || [];
// 每个区造一个 option 填进去(普通 function
districts.forEach(function (d) {
$district.append('<option value="' + d + '">' + d + '</option>');
});
});
你发现没有,省和市两段代码模板一模一样:清空下级 → 判空 return → 查数据 → 遍历生成 option。把这个模板背下来,四级、五级联动就是再加一层的事。注意那句 || []:如果某个城市没有区县数据,chinaData[省].cities[市] 会是 undefined,直接遍历就报错,用 || [] 兜个空数组就安全了。
| 方式 | 做法 | 适用 |
|---|---|---|
| 一次性加载 | 页面刚打开就把省市区全部数据放进前端 | 数据量小(<1000 条)、层级固定 |
| 按需加载 | 选省后才 AJAX 请求城市,选市后才请求区县 | 数据量大、层级深、要动态更新 |
// 按需加载示例:选了省份,才去请求它的城市 → 不一次性全拉,选啥要啥
$province.on("change", function () {
// 取选的省 id
const provinceId = $(this).val();
// 没选就别请求
if (!provinceId) return;
// 找后端要这个省的城市
$.ajax({
// 城市接口
url: "/api/cities",
// 把省 id 发给后端
data: { provinceId },
// 后端把城市列表送回来(普通 function)
success: function (res) {
// 0 表示成功
if (res.code === 0) {
// 先清空城市旧选项
$city.html('<option value="">请选择城市</option>');
// 把后端给的城市填进去(普通 function + 拼接)
res.data.forEach(function (c) {
$city.append('<option value="' + c.id + '">' + c.name + '</option>');
});
// 顺手把区县也清空 → 上级变了下级也得清
$district.html('<option value="">请选择区县</option>');
}
}
});
});
| 现象 | 原因 | 解决 |
|---|---|---|
| 选省后城市没变化 | 数据键名和选项值对不上(大小写、空格) | 检查键名和 value 必须完全一致 |
| 区县下拉是空的 | 该城市根本没有区县数据 | 用 || [] 兜底,空时提示"暂无区县" |
| 多切换几次出现重复选项 | 每次都 append,却没先清空 | 更新前先 .html("") 清空 |
① 可能在什么地方用:收货地址的省市区选择、企业注册的地区选择、商品分类的多级下拉、酒店预订的城市/商圈选择——凡是"一层选完下一层才知道有什么"的表单都是联动。
② 常见的问题:切换省份后区县残留旧数据(忘了清空区县);多切换几次选项越堆越多(每次 append 前没 .html("") 清空);选了城市区县空了还报错(数据为空时忘了 || [] 兜底);选省后城市一动不动(数据键名和 option 的 value 对不上)。
③ 解决思路:选省后在控制台打印一下 $(this).val() 和查出来的城市数组,看是不是空的、键名对不对;选项重复就检查有没有先清空;区县报错就在取值那行加 || []。记住模板:清空下级 → 判空 → 查数据 → 生成 option。
| API | 作用 | 参数怎么传 | 返回/结果 | 代码示例 |
|---|---|---|---|---|
| on("change") | 上级一变就触发联动 | 事件,函数 | 无 | $("#sel").on("change", function(){ ... }); |
| $("<option>") | 新造一个下拉选项 | ("<option>") | option 元素 | let $op = $("<option>"); |
| append | 把选项塞进下拉框 | ($op) | jQuery 对象 | $("#sel").append($op); |
| Object.keys | 取出所有省名 | (数据对象) | 省名数组 | let cities = Object.keys(data[prov]); |
| .html("") 清空 | 先清空旧选项再生成 | ("") | 下拉被清空 | $("#sel").html("<option>请选择</option>"); |