(根据居然老师直播课内容整理)
一、页面功能简介
- 在“用户中心”的任一界面,点击左边“收货地址”后,显示下面界面
- 此界面包括以下4个功能:
- 新增收货地址
- 删除当前收货地址
- 编辑(当前记录收货地址)
- 设为默认
- 以4个功能中,“新增收货地址”和“编辑” 需要弹出表单,进行编辑
二、显示收货地址信息页面
1、实现分析
- 在“用户中心”的任一界面,点击左边“收货地址”
- 前端向后端发起 user/addresses路由发起 get请求
- 后端接收请求,判断用户是否登录
- 如果已登录,返回页面
- 如果未登录,跳转到登录页面
2、后端veiw实现
# /apps/users/views.py
class AddressView(LoginRequiredMixin, View):
"""用户收货地址"""
def get(self, request):
"""提供收货地址界面"""
return render(request, 'user_center_site.html')
3、路由注册
# /apps/users/urls.py
# 展示用户地址
path('addresses/', views.AddressView.as_view(),name="addresses"),
三、收货地址数据模型
- 用户地址模型类定在users应用的models.py中
1、收货地址模型类
class Address(BaseModel):
"""用户地址"""
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='addresses', verbose_name='用户')
title = models.CharField(max_length=20, verbose_name='地址名称')
receiver = models.CharField(max_length=20, verbose_name='收货人')
province = models.ForeignKey('areas.Area', on_delete=models.PROTECT, related_name='province_addresses', verbose_name='省')
city = models.ForeignKey('areas.Area', on_delete=models.PROTECT, related_name='city_addresses', verbose_name='市')
district = models.ForeignKey('areas.Area', on_delete=models.PROTECT, related_name='district_addresses', verbose_name='区')
place = models.CharField(max_length=50, verbose_name='地址')
mobile = models.CharField(max_length=11, verbose_name='手机')
tel = models.CharField(max_length=20, null=True, blank=True, default='', verbose_name='固定电话')
email = models.CharField(max_length=30, null=True, blank=True, default='', verbose_name='电子邮箱')
is_deleted = models.BooleanField(default=False, verbose_name='逻辑删除')
class Meta:
db_table = 'tb_address'
verbose_name = '用户地址'
verbose_name_plural = verbose_name
ordering = ['-update_time']
2、收货地址模型类说明
- 收货地址模型类中省、市、县的外键指向areas/models里面的Area。
- 指明外键时,可以使用应用名.模型类名来定义,也可以使用模型类
- ordering表示在进行收货地址模型查询时,默认使用的排序方式。
- ordering = [’-update_time’] : 根据更新的时间倒叙。
3、补充用户模型默认地址字段
- 默认地址字段应保存到user表中,与用户绑定,需要修改User模型
4、数据库迁移
python manage.py makemigrations
python manage.py migrate
四、新增用户收货地址
1、接口设计和定义
1.1 请求方式:
选项 |
方案 |
请求方法 |
POST |
请求地址 |
/users/addresses/create/ |
1.2 请求参数 :
参数名 |
类型 |
是否必传 |
说明 |
eceiver |
string |
是 |
收货人 |
province_id |
string |
是 |
省份ID |
city_id |
string |
是 |
城市ID |
district_id |
string |
是 |
区县ID |
place |
string |
是 |
收货地址 |
mobile |
string |
是 |
手机号 |
tel |
string |
否 |
固定电话 |
email |
string |
否 |
邮箱 |
1.3 响应结果 : json
响应结果 |
响应内容 |
code |
状态码 |
errmsg |
错误信息 |
id |
地址ID |
receiver |
收货人 |
province |
省份名称 |
city |
城市名称 |
district |
区县名称 |
place |
收货地址 |
mobile |
手机号 |
tel |
固定电话 |
email |
邮箱 |
2、后端view实现
- 判断用户是否登录
- 接收参数
- 校验参数
- 必传参数量否传递
- 手机号校验
- 如果填有固定电话,固定电话需要校验
- 如果填了邮箱,邮箱需校验
- 保存到数据库
- 根据前端需要,返回详细数据
class AddressCreateView(LoginRequiredJSONMixin, View):
"""新增地址"""
def post(self,request):
# 接收参数
json_dict=json.loads(request.body.decode())
receiver = json_dict.get('receiver')
province_id = json_dict.get('province_id')
city_id = json_dict.get('city_id')
district_id = json_dict.get('district_id')
place = json_dict.get('place')
mobile = json_dict.get('mobile')
tel = json_dict.get('tel')
email = json_dict.get('email')
# 验证
if not all([receiver,province_id,city_id,district_id,place,mobile]):
return http.HttpResponseForbidden('缺少必传参数')
if not re.match(r"^1[3-9]\d{9}$", mobile):
return http.HttpResponseForbidden('参数mobile有误')
if tel and not re.match(r'^(0[0-9]{2,3}-)?([2-9][0-9]{6,7})+(-[0-9]{1,4})?$', tel):
return http.HttpResponseForbidden('参数固定电话有误')
if not email and not re.match(r'^[a-z0-9][\w\.\-]*@[a-z0-9\-]+(\.[a-z]{2,5}){1,2}$', email):
return http.HttpResponseForbidden('参数email有误')
# 保存
try:
address = Address.objects.create(
user=request.user,
title=receiver,
receiver=receiver,
province_id=province_id,
city_id=city_id,
district_id=district_id,
place=place,
mobile=mobile,
tel=tel,
email=email,
)
except Exception as e:
logging.error(e)
return http.HttpResponseServerError({"code": RETCODE.DBERR, "errmsg": "新增收获地址失败"})
# 响应
address_dict = {
"id": address.id,
"receiver": address.receiver,
"province": address.province.name,
"city": address.city.name,
"district": address.district.name,
"place": address.place,
"mobile": address.mobile,
"tel": address.tel,
"email": address.email
}
return http.JsonResponse({"code": RETCODE.OK, "errmsg": "新增收货地址成功", "address": address_dict})
3、定义路由
4、前端实现
4.1 新增地址JS
// 新增地址
save_address(){
if (this.error_receiver || this.error_place || this.error_mobile || this.error_email || !this.form_address.province_id || !this.form_address.city_id || !this.form_address.district_id ) {
alert('信息填写有误!');
} else {
// 新增地址
let url = '/users/addresses/create/';
axios.post(url, this.form_address, {
headers: {
'X-CSRFToken':getCookie('csrftoken')
},
responseType: 'json'
})
.then(response => {
if (response.data.code == '0') {
// 局部刷新界面:展示所有地址信息,将新的地址添加到头部
this.addresses.splice(0, 0, response.data.address);
this.is_show_edit = false;
} else if (response.data.code == '4101') {
location.href = '/login/?next=/users/addresses/';
} else {
alert(response.data.errmsg);
}
})
.catch(error => {
console.log(error.response);
})
}
},
4.2 新增地址html
<form>
<div class="form_group">
<label>*收货人:</label>
<input v-model="form_address.receiver" @blur="check_receiver" type="text" class="receiver">
<span v-show="error_receiver" class="receiver_error">请填写收件人</span>
</div>
<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>
<div class="form_group">
<label>*详细地址:</label>
<input v-model="form_address.place" @blur="check_place" type="text" class="place">
<span v-show="error_place" class="place_error">请填写地址信息</span>
</div>
<div class="form_group">
<label>*手机:</label>
<input v-model="form_address.mobile" @blur="check_mobile" type="text" class="mobile">
<span v-show="error_mobile" class="mobile_error">手机信息有误</span>
</div>
<div class="form_group">
<label>固定电话:</label>
<input v-model="form_address.tel" @blur="check_tel" type="text" class="tel">
<span v-show="error_tel" class="tel_error">固定电话有误</span>
</div>
<div class="form_group">
<label>邮箱:</label>
<input v-model="form_address.email" @blur="check_email" type="text" class="email">
<span v-show="error_email" class="email_error">邮箱信息有误</span>
</div>
<input @click="save_address" type="button" name="" value="新 增" class="info_submit">
<input @click="is_show_edit=false" type="reset" name="" value="取 消" class="info_submit info_reset">
</form>
5、后端优化
5.1 用户地址不能超过限制,提交后端后就需要判断
- 前端提交后,应首先判断用户收货地址数据量是否超过上限,
- 用户信息可以通过request.user得到
5.2 设置默认收货地址
- 新增第一个地址时,应该添加为默认收货地址
- 或者新增时,判断一下用户默认地址是否为空,如果为空,将收货地下添加为默认收货地址
五、完善显示收货地址页面
- 点击收货地址页面时,后端就应该将已有收货址信息传递给前端,以供显示
1、后端实现
1.1 展示地址请求方式:
选项 |
方案 |
请求方法 |
POST |
请求地址 |
/users/addresses/ |
1.2 展示地址请求参数: 无
1.3 展示地址响应结果:HTML,参数
参数为{“addresses”:[address字典,],“default_address_id”:缺省地址id}
1.4 代码实现:
- 获取当前用户的收货地址列表
- 循环生成每个地址字典组成的用户地址列表
- 将用户字典列表与缺省地址组合成参数
- 返回html和参数
class AddressView(LoginRequiredMixin, View):
"""用户收货地址"""
def get(self, request):
"""提供收货地址界面"""
login_user=request.user
addresses=Address.Objects.filter(user=login_user, is_deleted=False)
address_list = []
for address in addresses:
address_dict = {
'id': address.id,
'title': address.title,
'receiver': address.receiver,
'province': address.province.name,
'city': address.city.name,
'district': address.district.name,
'place': address.place,
'mobile': address.mobile,
'tel': address.tel,
'email': address.email
}
address_list.append(address_dict)
context = {
'addresses': address_list,
"default_address_id": request.user.default_address_id if request.user.default_address_id else 0
}
return render(request, 'user_center_site.html',context)
2、前端js
- 将后端模板数据传递到Vue.js
- /static/js/user_center_site.js
- /templates/user_center_site.html
3、user_center_site.html中渲染地址信息
<div class="right_content clearfix" v-cloak>
<div class="site_top_con">
<a @click="show_add_site">新增收货地址</a>
<span>你已创建了<b>[[ addresses.length ]]</b>个收货地址,最多可创建<b>20</b>个</span>
</div>
<div class="site_con" v-for="(address, index) in addresses">
<div class="site_title">
<h3>[[ address.title ]]</h3>
<a href="javascript:;" class="edit_icon"></a>
<em v-if="address.id===default_address_id">默认地址</em>
<span class="del_site">×</span>
</div>
<ul class="site_list">
<li><span>收货人:</span><b>[[ address.receiver ]]</b></li>
<li><span>所在地区:</span><b>[[ address.province ]] [[address.city]] [[ address.district ]]</b></li>
<li><span>地址:</span><b>[[ address.place ]]</b></li>
<li><span>手机:</span><b>[[ address.mobile ]]</b></li>
<li><span>固定电话:</span><b>[[ address.tel ]]</b></li>
<li><span>电子邮箱:</span><b>[[ address.email ]]</b></li>
</ul>
<div class="down_btn">
<a v-if="address.id!=default_address_id">设为默认</a>
<a href="javascript:;" class="edit_icon">编辑</a>
</div>
</div>
</div>
4、完善user_center_site.js中成功新增地址后的局部刷新
六、编辑收货地址
- 点击某条收货地址“编辑”按钮时,会显示用户地址修改界面
- 删除地址后端逻辑和新增地址后端逻辑非常的相似。
1、修改地址接口设计和定义
1.1 修改地址请求方式:
选项 |
方案 |
请求方法 |
PUT |
请求地址 |
/addresses/(?P<address_id>\d+)/ |
1.2 修改地址请求参数: 路径参数 和 JSON
参数名 |
类型 |
是否必传 |
说明 |
address_id |
string |
是 |
要修改的地址ID(路径参数) |
eceiver |
string |
是 |
收货人 |
province_id |
string |
是 |
省份ID |
city_id |
string |
是 |
城市ID |
district_id |
string |
是 |
区县ID |
place |
string |
是 |
收货地址 |
mobile |
string |
是 |
手机号 |
tel |
string |
否 |
固定电话 |
email |
string |
否 |
邮箱 |
1.3 修改地址响应结果:JSON
响应结果 |
响应内容 |
code |
状态码 |
errmsg |
错误信息 |
id |
地址ID |
receiver |
收货人 |
province |
省份名称 |
city |
城市名称 |
district |
区县名称 |
place |
收货地址 |
mobile |
手机号 |
tel |
固定电话 |
email |
邮箱 |
2、后端view实现
- 判断用户是否登录
- 接收参数
- 校验参数
- 必传参数量否传递
- 手机号校验
- 如果填有固定电话,固定电话需要校验
- 如果填了邮箱,邮箱需校验
- 更新用户地址
- 有两种方法:
- 方法一:Address.objects.get(id=address_id)得到对象,然后依次赋值
- 方法二:Address.objects.filter(id=address_id).update(参数)
- 构造响应数据
- 根据前端需要,返回详细数据
class UpdateDestoryAddressView(LoginRequiredJSONMixin, View):
"""更新和删除地址"""
def put(self, request, address_id):
json_dict = json.loads(request.body.decode())
receiver = json_dict.get('receiver')
province_id = json_dict.get('province_id')
city_id = json_dict.get('city_id')
district_id = json_dict.get('district_id')
place = json_dict.get('place')
mobile = json_dict.get('mobile')
tel = json_dict.get('tel')
email = json_dict.get('email')
if not all([receiver, province_id, city_id, district_id, place, mobile]):
return http.HttpResponseForbidden('缺少必传参数')
if not re.match(r"^1[3-9]\d{9}$", mobile):
return http.HttpResponseForbidden('参数mobile有误')
if tel and not re.match(r'^(0[0-9]{2,3}-)?([2-9][0-9]{6,7})+(-[0-9]{1,4})?$', tel):
return http.HttpResponseForbidden('参数固定电话有误')
if email and not re.match(r'^[a-z0-9][\w\.\-]*@[a-z0-9\-]+(\.[a-z]{2,5}){1,2}$', email):
return http.HttpResponseForbidden('参数email有误')
try:
Address.objects.filter(id=address_id,user=request.user).update(
user=request.user,
title=receiver,
receiver=receiver,
province_id=province_id,
city_id=city_id,
district_id=district_id,
place=place,
mobile=mobile,
tel=tel,
email=email,
)
except Exception as e:
return http.JsonResponse({
'code': RETCODE.DBERR, 'errmsg': '修改地址失败'})
address = Address.objects.get(id=address_id)
address_dict = {
'id': address.id,
'receiver': address.title,
'province': address.province.name,
'city': address.city.name,
'district': address.district.name,
'place': address.place,
'mobile': address.mobile,
'tel': address.tel,
'email': address.email
}
return http.JsonResponse({
'code': RETCODE.OK, 'errmsg': '修改地址成功', 'address': address_dict})
3、 路由定义
3、修改地址前端逻辑实现
3.1 添加修改地址的标记
- 新增地址和修改地址的交互不同。
- 为了区分用户是新增地址还是修改地址,我们可以选择添加一个变量,作为标记。
- 为了方便得到正在修改的地址信息,我们可以选择展示地址时对应的序号作为标记。
3.2 实现编辑按钮对应的事件
七、 删除收货地址
1、修改地址接口设计和定义
1.1 修改地址请求方式:
选项 |
方案 |
请求方法 |
DELETE |
请求地址 |
/addresses/(?P<address_id>\d+)/ |
1.2 修改地址请求参数: 路径参数 和 JSON
参数名 |
类型 |
是否必传 |
说明 |
address_id |
string |
是 |
要修改的地址ID(路径参数) |
1.3 修改地址响应结果:JSON
响应结果 |
响应内容 |
code |
状态码 |
errmsg |
错误信息 |
2、后端view实现
- 判断用户是否登录
- 获取登录用户对象
- 通过 Address.objects.get(id=address_id) 得到对象,将is_deleted赋值为 True
- 判断登录用户的缺省地址是否是删除对象
- 返回前端操作状态
class UpdateDestoryAddressView(LoginRequiredJSONMixin, View):
"""更新收获地址"""
def delete(self, request, address_id):
pass
def delete(self, request, address_id):
login_user=request.user
try:
address=Address.objects.get(id=address_id,user=login_user)
address.is_deleted=True
address.save()
if login_user.default_address_id==address.id:
login_user.default_address=None
login_user.save()
except Exception as e:
logger.error(e)
return http.JsonResponse({'code': RETCODE.DBERR, 'errmsg': '删除地址失败'})
return http.JsonResponse({'code': RETCODE.OK, 'errmsg': '删除地址成功'})
3、路由定义
- 删除与修改路由一样,只是方法不同,故共用一个路由
4、删除地址前端逻辑实现
- 后端返回成功后,删除address列表中当前序号的元素
- this.addresses.splice(起始序号,要删除的项目数量)
- /static/js/user_center_site.js
八、设置默认地址
1、接口设计和定义
1.1 请求方式:
选项 |
方案 |
请求方法 |
PUT |
请求地址 |
addresses/(?P<address_id>\d+)/default/ |
1.2 请求参数: 路径参数
参数名 |
类型 |
是否必传 |
说明 |
address_id |
string |
是 |
要修改的地址ID(路径参数) |
1.3 响应结果:JSON
响应结果 |
响应内容 |
code |
状态码 |
errmsg |
错误信息 |
2、后端view实现
- 判断用户是否登录
- 获取登录用户对象
- 通过 Address.objects.get(id=address_id,user=登录用户) 得到对象
- 判断登录用户的缺省地址是否是查询到的对象
- 将查询到的对象赋值给登录用户缺省地址,并保存
- 返回前端操作结果
class DefaultAddressView(LoginRequiredJSONMixin,View):
"""设置默认地址"""
def put(self,request,address_id):
login_user=request.user
try:
address=Address.objects.get(id=address_id,user=login_user)
# if not address:
# return http.JsonResponse({'code': RETCODE.DBERR, 'errmsg': '收货地址数据有误'})
login_user.default_address=address
login_user.save()
except Address.DoesNotExist as e:
logger.error(e)
return http.JsonResponse({'code': RETCODE.DBERR, 'errmsg': '设置默认收货地址失败'})
return http.JsonResponse({'code': RETCODE.OK, 'errmsg': '设置默认收货地址成功'})
3、路由定义
4、前端逻辑实现
- 默认地址标识实现方法:
- 专门定义了一个变量default_address_id,记录默认地址id
- 每条记录都有一个默认地址标识,只有当此行地址id=default_address_id时,默认地址标只才显示
- 当默认地址返回正确后,将该记录的地址id 赋值给default_address_id
九、修改地址标题
1、接口设计和定义
1.1 请求方式:
选项 |
方案 |
请求方法 |
PUT |
请求地址 |
addresses/(?P<address_id>\d+)/title/ |
1.2 请求参数: 路径参数 和 JSON
参数名 |
类型 |
是否必传 |
说明 |
address_id |
string |
是 |
要修改的地址ID(路径参数) |
title |
string |
是 |
要修改的 title名称 |
1.3 响应结果:JSON
响应结果 |
响应内容 |
code |
状态码 |
errmsg |
错误信息 |
2、后端view实现
- 判断用户是否登录
- 获取参数:put 参数在request.body中
- address_id是路径参数,路由解析得到
- json_dict=json.loads(request.body.decode())
- 校验参数
- 获取登录用户对象
- 通过 Address.objects.get(id=address_id,user=登录用户) 得到对象
- 将title的值赋给对象的title,并保存
- 返回前端操作结果
class TitleAddressView(LoginRequiredJSONMixin,View):
"""设置地址标题"""
def put(self,request,address_id):
login_user=request.user
json_dict=json.loads(request.body.decode())
if not json_dict or not json_dict["title"]:
return http.HttpResponseForbidden('缺少必传参数')
try:
address=Address.objects.get(user=login_user,id=address_id)
address.title=json_dict["title"]
address.save()
except Address.DoesNotExist as e:
logger.error(e)
return http.JsonResponse({'code': RETCODE.DBERR, 'errmsg': '收货地址标题保存失败'})
return http.JsonResponse({'code': RETCODE.OK, 'errmsg': '收货地址标题保存成功'})
3、路由定义
4、前端逻辑实现
- 定义一个变量edit_title_index,用于控制编辑框显示
- 每个地址信息上方都有编辑框和按钮,只有当edit_title_index=当前idex时,才会显示
4.1 用超链接点击事件,显示编辑框及按钮
4.2 编辑框及保存取消按钮显示
4.2 取消和保存处理