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