功能需求介绍:
在网站开发的过程中,难免会遇到类似于"省市区"这样的多级联动的下拉框的功能存在,比如在电商的网站开发中,用户购买了东西需要填写收货地址这样的情况。为了给用户提供一个比较方便比较好的体验感,很多网站都采用了三级联动这样的一个效果,也就是当用户选择了某一个省份之后,再去选择市的时候,市的那个下拉框只会弹出属于这个省下的市,而不会出现别的省份的市。
那么接下来就来聊一聊,在我的电商的项目中是如何去实现这样的一个功能的。
首先是表结构的设计,这里,我采用的是自关联的方式,将省市区的所有数据都存在一张表里面,表结构代码如下。
# models.py
class Area(models.Model):
"""
省市区
Area实例对象.area_set.all() 查询该区域的所有子级行政列表
设置了related_name后,就把area_set变成了subs了,Area实例对象.subs.all()
A 1—— B 多
A.B_set.all()
"""
name = models.CharField(max_length=20, verbose_name="区域名称")
parent = models.ForeignKey('self', on_delete=models.SET_NULL, related_name='subs', null=True, blank=True,
verbose_name="父级行政区域") # 外键自关联自己
class Meta:
db_table = 'tb_areas'
verbose_name = '省市区'
def __str__(self):
return self.name
介绍完表结构之后,来讲讲前后端的业务逻辑是怎么样的。
前端采用用vue和ajax来监听省市区下拉框的变化情况从而做出反应。
# user_center_address.html
<div class="form_group">
<label>*所在地区:</label>
<select v-model="form_address.province_id">
<option v-for="province in provinces" :value="province.id">[[ province.name ]]</option>
</select>
<select v-model="form_address.city_id">
<option v-for="city in cities" :value="city.id">[[ city.name ]]</option>
</select>
<select v-model="form_address.district_id">
<option v-for="district in districts" :value="district.id">[[ district.name ]]</option>
</select>
</div>
当第一次点击新增地址的时候,会触发vue中的get_provinces()方法,像后端发起请求,获取省市区的数据信息。
# user_center_address.js
get_provinces(){
console.log("hots:"+this.host);
var url = this.host + '/areas/';
console.log(url);
axios.get(url, {
responseType: 'json'
})
.then(response => {
console.log(response);
console.log("wozaizheli");
if (response.data.code == '0') {
console.log("haha1");
this.provinces = response.data.province_list;
} else {
console.log("haha2");
console.log(response.data);
this.provinces = [];
}
})
.catch(error => {
console.log("haha3");
console.log(error.response);
this.provinces = [];
});
},
同时前端还监听"省市"下拉框是否发生变化的事件,如果发生变化,也会向后端发送请求。
# user_center_address.js
watch: {
// 监听到省份id变化
'form_address.province_id': function () {
if (this.form_address.province_id) {
var url = this.host + '/areas/?area_id=' + this.form_address.province_id;
axios.get(url, {
responseType: 'json'
})
.then(response => {
if (response.data.code == '0') {
this.cities = response.data.sub_data.subs;
} else {
console.log(response.data);
this.cities = [];
}
})
.catch(error => {
console.log(error.response);
this.cities = [];
});
}
},
// 监听到城市id变化
'form_address.city_id': function () {
if (this.form_address.city_id) {
var url = this.host + '/areas/?area_id=' + this.form_address.city_id;
axios.get(url, {
responseType: 'json'
})
.then(response => {
if (response.data.code == '0') {
this.districts = response.data.sub_data.subs;
} else {
console.log(response.data);
this.districts = [];
}
})
.catch(error => {
console.log(error.response);
this.districts = [];
});
}
}
},
前端我们就简单的介绍一下,接下来,来看看后端的实现逻辑吧。
直接上代码
# views.py
# 1.提取 area_id
area_id = request.GET.get('area_id')
# 2.判断 area_id 是否存在
# 2.3组织响应数据
if not area_id:
province_list = cache.get("province_list")
if not province_list:
# 2.1如果前端没有传入area_id,表示用户需要省份数据,那么查询 parent_id 等于 null 的数据
try:
provinces = Area.objects.filter(parent__isnull=True)
print("in here!!!!")
province_list = []
for province in provinces:
province_list.append({'id': province.id, 'name': province.name})
except Exception:
return JsonResponse({'code': RETCODE.DBERR, 'errmsg': '省份数据错误'})
cache.set("province_list", province_list, constants.AREAS_CACHE_EXPIRES)
return JsonResponse({'code': RETCODE.OK, 'errmsg': 'ok', 'province_list': province_list})
else:
sub_data = cache.get("sub_" + area_id)
print(sub_data, type(sub_data))
if not sub_data:
# 2.2如果前端传入了area_id,表示用户需要市或区数据,那么查询 parent_id 等于 area_id 的数据
try:
area = Area.objects.get(id=area_id)
subs = area.subs.all()
subs_list = []
for sub in subs:
subs_list.append({'id': sub.id, 'name': sub.name})
sub_data = {'id': area.id, 'name': area.name, 'subs': subs_list}
print("subs_list: ", subs_list)
except Exception:
return JsonResponse({'code': RETCODE.DBERR, 'errmsg': '市区数据错误'})
cache.set("sub_" + area_id, sub_data, constants.AREAS_CACHE_EXPIRES)
return JsonResponse({'code': RETCODE.OK, 'errmsg': 'ok', 'sub_data': sub_data})
后端这边呢,我采用数据库+缓存结合查询的方式。为什么这么做呢?
原因很简单,因为"省市区"的数据不是频繁变动的数据,所以没必要每一次都去数据库重新查询。
好,接下来分析下后端的逻辑
首先当第一次点开新增地址的操作时,浏览器会发一个url请求到后端,但第一次的url请求是不带area_id的,所以会进if分支,先去缓存中看看有没有省份的数据存在,如果没有,再去查询数据库把所有省的数据取出来,写入缓存,最后返回给前端页面;接下来,前端用户选择了一个省份,再一次触发了url请求,这一次的请求是带有area_id的,所以会进入else分支,同样的也是先去缓存中查该省份的市数据是否有,如果没有再去数据库查,再写入缓存,最后返回给前端。
三级联动的功能实现的精华是在表结构的巧妙设计和前后端逻辑的搭配,希望给大家一起启发。