省市区三级联动选择器:免费API与前端框架实战指南
每次开发表单系统时,最头疼的就是省市区选择器的数据维护问题。手动维护不仅耗时耗力,还要应对行政区划的频繁调整。本文将介绍如何利用免费API快速构建动态加载的三级联动选择器,并提供Vue和React两种主流框架的完整实现方案。
1. 为什么需要动态行政区划API
传统的前端省市区选择器通常采用静态JSON数据,这种方式存在几个明显缺陷:
- 数据更新滞后:行政区划每年都有调整(如撤县设区、新设地级市等),静态数据需要手动更新
- 体积臃肿:完整的省市区数据JSON文件可能达到几百KB,影响首屏加载速度
- 维护成本高:需要定期检查数据准确性,跨项目复用困难
相比之下,动态API方案具有以下优势:
| 对比维度 | 静态JSON方案 | 动态API方案 |
|---|---|---|
| 数据时效性 | 依赖手动更新 | 实时最新 |
| 网络传输 | 一次性加载全部数据 | 按需加载 |
| 维护成本 | 高 | 低 |
| 跨项目复用 | 需要复制文件 | 直接调用接口 |
adcode(行政区划代码)是这个系统的关键,它是国家标准的行政区划唯一标识,由6位数字组成:
- 前2位:省/直辖市代码
- 中间2位:地级市代码
- 后2位:区县代码
2. 免费行政区划API详解
我们推荐使用高德地图开放平台的行政区划查询API,它具有以下特点:
- 完全免费(每日限额足够一般应用使用)
- 数据权威,与国家统计局同步更新
- 无需注册小程序或签到获取密钥
2.1 API基础配置
const API_URL = 'https://restapi.amap.com/v3/config/district' const API_KEY = '您申请的高德开发者key' // 建议存储在环境变量中 // 基础请求参数 const baseParams = { key: API_KEY, extensions: 'all', // 获取全部子级 subdistrict: 3, // 递归获取三级行政区 }提示:高德开发者Key可通过注册高德开放平台免费获取,个人开发者每日有3000次免费调用额度。
2.2 接口响应数据结构
典型响应示例(山东省济南市):
{ "status": "1", "info": "OK", "districts": [ { "citycode": "0531", "adcode": "370100", "name": "济南市", "level": "city", "districts": [ { "citycode": "0531", "adcode": "370102", "name": "历下区", "level": "district", "districts": [] } // 其他区县... ] } ] }关键字段说明:
citycode:区号(如北京010)adcode:行政区划代码level:行政级别(province/city/district)districts:下级行政区数组
3. Vue 3实现方案
3.1 组件基础结构
使用Vue 3的Composition API和<script setup>语法:
<template> <div class="cascader"> <select v-model="selectedProvince" @change="loadCities"> <option value="">请选择省份</option> <option v-for="province in provinces" :value="province.adcode"> {{ province.name }} </option> </select> <select v-model="selectedCity" @change="loadDistricts" :disabled="!selectedProvince"> <option value="">请选择城市</option> <option v-for="city in cities" :value="city.adcode"> {{ city.name }} </option> </select> <select v-model="selectedDistrict" :disabled="!selectedCity"> <option value="">请选择区县</option> <option v-for="district in districts" :value="district.adcode"> {{ district.name }} </option> </select> </div> </template>3.2 数据加载逻辑
<script setup> import { ref, onMounted } from 'vue' const provinces = ref([]) const cities = ref([]) const districts = ref([]) const selectedProvince = ref('') const selectedCity = ref('') const selectedDistrict = ref('') // 加载省份数据 const loadProvinces = async () => { const response = await fetch(`${API_URL}?keywords=中国&${new URLSearchParams(baseParams)}`) const data = await response.json() provinces.value = data.districts[0].districts } // 加载城市数据 const loadCities = async () => { const response = await fetch(`${API_URL}?keywords=${selectedProvince.value}&${new URLSearchParams(baseParams)}`) const data = await response.json() cities.value = data.districts[0]?.districts || [] districts.value = [] selectedCity.value = '' selectedDistrict.value = '' } // 加载区县数据 const loadDistricts = async () => { const response = await fetch(`${API_URL}?keywords=${selectedCity.value}&${new URLSearchParams(baseParams)}`) const data = await response.json() districts.value = data.districts[0]?.districts || [] selectedDistrict.value = '' } onMounted(() => { loadProvinces() }) </script>4. React实现方案
4.1 使用自定义Hook封装逻辑
// useDistrict.js import { useState, useEffect } from 'react' export function useDistrict(apiKey) { const [provinces, setProvinces] = useState([]) const [cities, setCities] = useState([]) const [districts, setDistricts] = useState([]) const [selectedProvince, setSelectedProvince] = useState('') const [selectedCity, setSelectedCity] = useState('') const baseParams = { key: apiKey, extensions: 'all', subdistrict: 1, // 每次只获取下一级 } const fetchDistricts = async (keywords) => { const params = new URLSearchParams({...baseParams, keywords}) const response = await fetch(`${API_URL}?${params}`) return (await response.json()).districts[0]?.districts || [] } useEffect(() => { fetchDistricts('中国').then(setProvinces) }, []) useEffect(() => { if (!selectedProvince) return fetchDistricts(selectedProvince).then(setCities) setDistricts([]) setSelectedCity('') }, [selectedProvince]) useEffect(() => { if (!selectedCity) return fetchDistricts(selectedCity).then(setDistricts) }, [selectedCity]) return { provinces, cities, districts, selectedProvince, selectedCity, setSelectedProvince, setSelectedCity } }4.2 组件实现
// DistrictSelector.jsx import { useDistrict } from './useDistrict' export function DistrictSelector({ apiKey }) { const { provinces, cities, districts, selectedProvince, selectedCity, setSelectedProvince, setSelectedCity } = useDistrict(apiKey) return ( <div className="cascader"> <select value={selectedProvince} onChange={(e) => setSelectedProvince(e.target.value)} > <option value="">请选择省份</option> {provinces.map(province => ( <option key={province.adcode} value={province.adcode}> {province.name} </option> ))} </select> <select value={selectedCity} onChange={(e) => setSelectedCity(e.target.value)} disabled={!selectedProvince} > <option value="">请选择城市</option> {cities.map(city => ( <option key={city.adcode} value={city.adcode}> {city.name} </option> ))} </select> <select disabled={!selectedCity}> <option value="">请选择区县</option> {districts.map(district => ( <option key={district.adcode} value={district.adcode}> {district.name} </option> ))} </select> </div> ) }5. 性能优化与高级功能
5.1 数据缓存策略
避免重复请求已加载的数据:
// Vue示例 const cache = new Map() const fetchWithCache = async (key, fetchFn) => { if (cache.has(key)) return cache.get(key) const data = await fetchFn() cache.set(key, data) return data } // 修改loadCities函数 const loadCities = async () => { const key = `city_${selectedProvince.value}` const data = await fetchWithCache(key, async () => { const response = await fetch(`${API_URL}?keywords=${selectedProvince.value}&${new URLSearchParams(baseParams)}`) return (await response.json()).districts[0]?.districts || [] }) cities.value = data }5.2 防抖与加载状态
// React示例 import { useDebounce } from 'use-debounce' function useDistrict(apiKey) { const [loading, setLoading] = useState(false) const [debouncedProvince] = useDebounce(selectedProvince, 300) useEffect(() => { if (!debouncedProvince) return setLoading(true) fetchDistricts(debouncedProvince) .then(setCities) .finally(() => setLoading(false)) }, [debouncedProvince]) return { // ...其他返回值 loading } } // 在组件中使用 {loading && <span className="loading">加载中...</span>}5.3 完整地址回填功能
// 根据adcode获取完整路径 const getFullPath = async (adcode) => { const path = [] let current = adcode while (current && current !== '100000') { const response = await fetch(`${API_URL}?keywords=${current}&${new URLSearchParams(baseParams)}`) const data = await response.json() if (!data.districts[0]) break const { name, adcode, parentCode } = data.districts[0] path.unshift({ name, adcode }) current = parentCode } return path } // 使用示例 getFullPath('370102').then(path => { // 输出: [{name: "山东省", adcode: "370000"}, {name: "济南市", adcode: "370100"}, ...] })6. 错误处理与边界情况
实际项目中需要考虑的各种异常情况:
- API限流处理:
const fetchDistricts = async (keywords) => { try { const response = await fetch(`${API_URL}?keywords=${keywords}&${new URLSearchParams(baseParams)}`) const data = await response.json() if (data.status !== '1') { throw new Error(data.info || '请求失败') } return data.districts[0]?.districts || [] } catch (error) { console.error('获取行政区划失败:', error) // 显示友好错误提示 return [] } }- 特殊行政区划处理:
- 直辖市(北京/上海/天津/重庆)的层级结构特殊
- 港澳台地区的adcode规则不同
- 某些县级市直接隶属于省份(如河南省济源市)
- 离线回退方案:
// 当API不可用时使用本地缓存 const loadProvinces = async () => { try { const onlineData = await fetchProvincesFromAPI() localStorage.setItem('provinces_cache', JSON.stringify(onlineData)) return onlineData } catch { const cached = localStorage.getItem('provinces_cache') return cached ? JSON.parse(cached) : [] } }在最近的一个电商项目中,我们采用这种动态加载方案后,表单加载速度提升了40%,而且再也不用担心用户反馈"找不到新设立的区县"问题了。特别是在处理政府类项目时,行政区划数据的准确性直接关系到业务合规性,动态API方案成为了我们的首选。