省市区 + 地址管理
后端代码
Areas 模块
新建 apps/areas
进入/meiduo_mall/apps目录下:
python ../manage.py startapp areas
apps/areas/models.py
from django.db import models
class Area(models.Model):
"""
省 / 市 / 区
"""
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 = '省市区'
verbose_name_plural = verbose_name
def __str__(self):
return self.name
apps/areas/views.py
from django import http
from django.views import View
from django.core.cache import cache
from apps.areas.models import Area
from utils.response_code import RETCODE
class AreasView(View):
"""
GET /areas/ -> 省列表
GET /areas/?area_id=xxxx -> 市 / 区列表
"""
def get(self, request):
area_id = request.GET.get('area_id')
# 获取省
if not area_id:
province_list = cache.get('province_list')
if province_list is None:
provinces = Area.objects.filter(parent__isnull=True)
province_list = [
{'id': p.id, 'name': p.name}
for p in provinces
]
cache.set('province_list', province_list, 24 * 3600)
return http.JsonResponse({
'code': RETCODE.OK,
'errmsg': 'ok',
'province_list': province_list
})
# 获取市 / 区
sub_list = cache.get(f'sub_{area_id}')
if sub_list is None:
subs = Area.objects.filter(parent_id=area_id)
sub_list = [
{'id': s.id, 'name': s.name}
for s in subs
]
cache.set(f'sub_{area_id}', sub_list, 24 * 3600)
return http.JsonResponse({
'code': RETCODE.OK,
'errmsg': 'ok',
'sub_data': sub_list
})
apps/areas/urls.py
from django.urls import path
from .views import AreasView
app_name = "areas"
urlpatterns = [
path('areas/', AreasView.as_view(), name='areas'),
]
apps/areas/apps.py
from django.apps import AppConfig
class AreasConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'apps.areas'
settings.py 注册
INSTALLED_APPS = [
...
'apps.areas',
]
Users 模块(Address 相关完整代码)
apps/users/models.py
from django.db import models
from django.contrib.auth.models import AbstractUser
.。≥
from apps import areas
from utils.models import BaseModel
class User(AbstractUser):
mobile = models.CharField(max_length=11, unique=True, verbose_name='手机号')
email_active = models.BooleanField(default=False, verbose_name='邮箱激活状态')
default_address = models.ForeignKey(
'Address',
related_name='users',
null=True,
blank=True,
on_delete=models.SET_NULL,
verbose_name='默认地址'
)
class Meta:
db_table = 'tb_users'
verbose_name = '用户'
verbose_name_plural = verbose_name
def __str__(self):
return self.username
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, blank=True, default='', verbose_name='固定电话')
email = models.CharField(max_length=30, 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']
Address 视图
apps/users/views.py
# ============================================================
# Address 视图
# ============================================================
import json
import re
import logging
from django import http
from django.views import View
from utils.response_code import RETCODE
from utils.mixins import LoginRequiredJSONMixin
from apps.users.models import Address
logger = logging.getLogger('django')
# apps/users/views.py
import json
from django.shortcuts import render
from django import http
from django.views import View
from utils.mixins import LoginRequiredJSONMixin
from utils.response_code import RETCODE
from .models import Address
class AddressView(LoginRequiredMixin, View):
def get(self, request):
addresses = Address.objects.filter(
user=request.user,
is_deleted=False
)
address_list = []
for addr in addresses:
address_list.append({
'id': addr.id,
'title': addr.title,
'receiver': addr.receiver,
'province': addr.province.name,
'province_id': addr.province_id,
'city': addr.city.name,
'city_id': addr.city_id,
'district': addr.district.name,
'district_id': addr.district_id,
'place': addr.place,
'mobile': addr.mobile,
'tel': addr.tel,
'email': addr.email,
})
context = {
'addresses': address_list,
'default_address_id': request.user.default_address_id
}
return render(request, 'users/user_center_site.html', context)
class AddressCreateView(LoginRequiredJSONMixin, View):
def post(self, request):
data = json.loads(request.body.decode())
# ============================
# 1️⃣ 地址数量限制(≤ 20)
# ============================
address_count = Address.objects.filter(
user=request.user,
is_deleted=False
).count()
if address_count >= 20:
return http.JsonResponse({
'code': RETCODE.PARAMERR,
'errmsg': '地址数量已达上限(20 个)'
})
# ============================
# 2️⃣ 创建地址
# ============================
try:
address = Address.objects.create(
user=request.user,
title=data.get('title'),
receiver=data.get('receiver'),
province_id=data.get('province_id'),
city_id=data.get('city_id'),
district_id=data.get('district_id'),
place=data.get('place'),
mobile=data.get('mobile'),
tel=data.get('tel'),
email=data.get('email'),
)
except Exception as e:
logger.error(e)
return http.JsonResponse({
'code': RETCODE.DBERR,
'errmsg': '保存失败'
})
# ============================
# 3️⃣ 如果是第一个地址 → 设为默认
# ============================
if address_count == 0:
request.user.default_address = address
request.user.save()
# ============================
# 4️⃣ 返回数据
# ============================
return http.JsonResponse({
'code': RETCODE.OK,
'errmsg': 'ok',
'address': {
'id': address.id,
'title': address.title,
'receiver': address.receiver,
'province': address.province.name,
'city': address.city.name,
'district': address.district.name,
'province_id': address.province_id,
'city_id': address.city_id,
'district_id': address.district_id,
'place': address.place,
'mobile': address.mobile,
'tel': address.tel,
'email': address.email,
}
})
# 更新/删除 地址
class AddressUpdateView(LoginRequiredJSONMixin, View):
def put(self, request, address_id):
data = json.loads(request.body.decode())
try:
address = Address.objects.get(
id=address_id,
user=request.user,
is_deleted=False
)
except Address.DoesNotExist:
return http.JsonResponse({
'code': RETCODE.NODATAERR,
'errmsg': '地址不存在'
})
# ===== 手动字段映射(关键)=====
address.title = data.get('title', address.title)
address.receiver = data.get('receiver', address.receiver)
address.place = data.get('place', address.place)
address.mobile = data.get('mobile', address.mobile)
address.tel = data.get('tel', address.tel)
address.email = data.get('email', address.email)
# 外键要用 _id 赋值
address.province_id = data.get('province_id', address.province_id)
address.city_id = data.get('city_id', address.city_id)
address.district_id = data.get('district_id', address.district_id)
address.save()
# 返回给前端的数据(和新增保持一致)
return http.JsonResponse({
'code': RETCODE.OK,
'errmsg': 'ok',
'address': {
'id': address.id,
'title': address.title,
'receiver': address.receiver,
'province': address.province.name,
'city': address.city.name,
'district': address.district.name,
'province_id': address.province_id,
'city_id': address.city_id,
'district_id': address.district_id,
'place': address.place,
'mobile': address.mobile,
'tel': address.tel,
'email': address.email,
}
})
def delete(self, request, address_id):
"""
删除地址(逻辑删除)
"""
try:
address = Address.objects.get(
id=address_id,
user=request.user,
is_deleted=False
)
except Address.DoesNotExist:
return http.JsonResponse({
'code': RETCODE.NODATAERR,
'errmsg': '地址不存在'
})
address.is_deleted = True
address.save()
return http.JsonResponse({
'code': RETCODE.OK,
'errmsg': 'ok'
})
# 设为默认地址
class DefaultAddressView(LoginRequiredJSONMixin, View):
def put(self, request, address_id):
try:
address = Address.objects.get(
id=address_id,
user=request.user
)
except Address.DoesNotExist:
return http.JsonResponse({
'code': RETCODE.NODATAERR,
'errmsg': '地址不存在'
})
request.user.default_address = address
request.user.save()
return http.JsonResponse({'code': RETCODE.OK, 'errmsg': 'ok'})
class AddressTitleView(LoginRequiredJSONMixin, View):
def put(self, request, address_id):
data = json.loads(request.body.decode())
title = data.get('title')
if not title:
return http.JsonResponse({
'code': RETCODE.PARAMERR,
'errmsg': '缺少 title'
})
Address.objects.filter(
id=address_id,
user=request.user
).update(title=title)
return http.JsonResponse({'code': RETCODE.OK, 'errmsg': 'ok'})
apps/users/urls.py
from django.urls import path
from .views import (
AddressView,
AddressUpdateView,
DefaultAddressView,
AddressTitleView,
)
urlpatterns = [
# 用户注册登录相关
# 用户名重复检查(AJAX)
# ===== 图形验证码 =====
# 保存邮箱(PUT)
# 邮箱激活
# 收货地址
path('addresses/', AddressView.as_view(), name='addresses'), # 页面
path('addresses/create/', AddressCreateView.as_view()), # POST API
path('addresses/<int:address_id>/', AddressUpdateView.as_view()),
path('addresses/<int:address_id>/default/', DefaultAddressView.as_view()),
path('addresses/<int:address_id>/title/', AddressTitleView.as_view()),
]
抽象基类模型建议之后统一放在 utils.models.py 下面:
utils/models.py
from django.db import models
class BaseModel(models.Model):
"""
抽象基类模型
"""
create_time = models.DateTimeField(auto_now_add=True, verbose_name="创建时间")
update_time = models.DateTimeField(auto_now=True, verbose_name="更新时间")
class Meta:
abstract = True
数据库迁移
python manage.py makemigrations areas
python manage.py makemigrations users
python manage.py migrate
areas.sql 导入
mysql -u root -p meiduo_mall < areas.sql
后端部署总结
你现在的状态是:
数据库表已创建
tb_areastb_addresstb_users.default_address
后端接口已就绪
/areas//areas/?area_id=xxx/addresses/create(GET / POST)/addresses/<id>/(PUT / DELETE)/addresses/<id>/default//addresses/<id>/title/
正确分工必须是:
| URL | 类型 | 用途 | 谁在用 |
|---|---|---|---|
/users/addresses/ | 页面 GET | 收货地址管理页面 | 浏览器跳转 |
/users/addresses/create/ | POST API | 新增收货地址 | Vue |
/users/addresses/<id>/ | PUT / DELETE API | 修改 / 删除地址 | Vue |
/users/addresses/<id>/default/ | PUT API | 设为默认地址 | Vue |
/users/addresses/<id>/title/ | PUT API | 修改地址标题 | Vue |
浏览器直接打开:http://127.0.0.1:8000/areas/
应该看到类似:
{
"code": "0",
"errmsg": "ok",
"province_list": [
{"id": 110000, "name": "北京市"},
{"id": 120000, "name": "天津市"}
]
}

访问:http://127.0.0.1:8000/areas/?area_id=110000

前端代码:
templates/users/user_center_site.html
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en">
<head>
<meta http-equiv="Content-Type" content="text/html;charset=UTF-8">
<title>美多商城-用户中心</title>
<link rel="stylesheet" type="text/css" href="../static/css/reset.css">
<link rel="stylesheet" type="text/css" href="../static/css/main.css">
<script type="text/javascript" src="../static/js/host.js"></script>
<script type="text/javascript" src="../static/js/vue-2.5.16.js"></script>
<script type="text/javascript" src="../static/js/axios-0.18.0.min.js"></script>
<script type="text/javascript">
let addresses = {{ addresses | safe }};
let default_address_id = "{{ default_address_id }}";
</script>
</head>
<body>
<div id="app" v-cloak>
<div class="header_con">
<div class="header">
<div class="welcome fl">欢迎来到美多商城!</div>
<div class="fr">
<div class="login_info fl">
欢迎您:<em>张 山</em>
</div>
<div class="login_btn fl">
<a href="login.html">登录</a>
<span>|</span>
<a href="register.html">注册</a>
</div>
<div class="user_link fl">
<span>|</span>
<a href="user_center_info.html">用户中心</a>
<span>|</span>
<a href="cart.html">我的购物车</a>
<span>|</span>
<a href="../static/user_center_order.html">我的订单</a>
</div>
</div>
</div>
</div>
<div class="search_bar clearfix">
<a href="index.html" class="logo fl"><img src="../static/images/logo.png"></a>
<div class="search_wrap fl">
<form method="get" action="/search/" class="search_con">
<input type="text" class="input_text fl" name="q" placeholder="搜索商品">
<input type="submit" class="input_btn fr" name="" value="搜索">
</form>
<ul class="search_suggest fl">
<li><a href="#">索尼微单</a></li>
<li><a href="#">优惠15元</a></li>
<li><a href="#">美妆个护</a></li>
<li><a href="#">买2免1</a></li>
</ul>
</div>
</div>
<div class="main_con clearfix">
<div class="left_menu_con clearfix">
<h3>用户中心</h3>
<ul>
<li><a href="user_center_info.html">· 个人信息</a></li>
<li><a href="../static/user_center_order.html">· 全部订单</a></li>
<li><a href="user_center_site.html" class="active">· 收货地址</a></li>
<li><a href="../static/user_center_pass.html">· 修改密码</a></li>
</ul>
</div>
<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" @click="delete_address(index)">×</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" @click="show_edit_site(index)">编辑</a>
</div>
</div>
</div>
</div>
<div class="footer">
<div class="foot_link">
<a href="#">关于我们</a>
<span>|</span>
<a href="#">联系我们</a>
<span>|</span>
<a href="#">招聘人才</a>
<span>|</span>
<a href="#">友情链接</a>
</div>
<p>CopyRight © 2016 北京美多商业股份有限公司 All Rights Reserved</p>
<p>电话:010-****888 京ICP备*******8号</p>
</div>
<div class="pop_con" v-show="is_show_edit" v-cloak>
<div class="site_con site_pop">
<div class="site_pop_title">
<h3 v-if="editing_address_index">编辑收货地址</h3>
<h3 v-else>新增收货地址</h3>
<a @click="is_show_edit=false">×</a>
</div>
<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 value="">请选择省</option>
<option
v-for="province in provinces"
:key="province.id"
:value="province.id">
[[ province.name ]]
</option>
</select>
<!-- 市 -->
<select v-model="form_address.city_id">
<option value="">请选择市</option>
<option
v-for="city in cities"
:key="city.id"
:value="city.id">
[[ city.name ]]
</option>
</select>
<!-- 区 -->
<select v-model="form_address.district_id">
<option value="">请选择区</option>
<option
v-for="district in districts"
:key="district.id"
: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>
</div>
</div>
<div class="pop_con2">
<div class="confirm_pop">
<div class="confirm_pop_title">
<h3>确认删除</h3>
<a href="javascript:;">×</a>
</div>
<p>您确认删除当前地址吗?</p>
<input type="button" value="确 定" class="confirm_submit" />
<input type="button" value="取 消" class="confirm_submit confirm_cancel" />
</div>
<div class="mask"></div>
</div>
</div>
<script type="text/javascript" src="../static/js/host.js"></script>
<script type="text/javascript" src="../static/js/common.js"></script>
<script type="text/javascript" src="../static/js/user_center_site.js"></script>
</body>
</html>
static/js/user_center_site.js
var vm = new Vue({
el: '#app',
delimiters: ['[[', ']]'],
data: {
host,
is_show_edit: false,
// 省市区数据
provinces: [],
cities: [],
districts: [],
// 地址列表
addresses: [],
default_address_id: null,
// 编辑状态
editing_address_index: '',
// 表单数据
form_address: {
title: '',
receiver: '',
province_id: '',
city_id: '',
district_id: '',
place: '',
mobile: '',
tel: '',
email: '',
}
},
mounted() {
// Django 注入的数据
this.addresses = JSON.parse(JSON.stringify(addresses));
this.default_address_id = default_address_id;
// 加载省份
this.get_provinces();
},
watch: {
// 省 -> 市
'form_address.province_id'(val) {
if (!val) {
this.cities = [];
this.districts = [];
return;
}
axios.get(this.host + '/areas/?area_id=' + val)
.then(res => {
if (res.data.code === '0') {
this.cities = res.data.sub_data;
this.districts = [];
}
});
},
// 市 -> 区
'form_address.city_id'(val) {
if (!val) {
this.districts = [];
return;
}
axios.get(this.host + '/areas/?area_id=' + val)
.then(res => {
if (res.data.code === '0') {
this.districts = res.data.sub_data;
}
});
}
},
methods: {
// 获取省份
get_provinces() {
axios.get(this.host + '/areas/')
.then(res => {
if (res.data.code === '0') {
this.provinces = res.data.province_list;
}
});
},
// 显示新增弹窗
show_add_site() {
this.is_show_edit = true;
this.editing_address_index = '';
this.form_address = {
title: '',
receiver: '',
province_id: '',
city_id: '',
district_id: '',
place: '',
mobile: '',
tel: '',
email: '',
};
this.cities = [];
this.districts = [];
},
// 显示编辑弹窗
show_edit_site(index) {
this.is_show_edit = true;
this.editing_address_index = index;
// 深拷贝,避免污染原数据
this.form_address = JSON.parse(JSON.stringify(this.addresses[index]));
// 回显省市区
axios.get(this.host + '/areas/?area_id=' + this.form_address.province_id)
.then(res => {
this.cities = res.data.sub_data;
});
axios.get(this.host + '/areas/?area_id=' + this.form_address.city_id)
.then(res => {
this.districts = res.data.sub_data;
});
},
// 保存地址(新增 / 修改)
save_address() {
let url;
let method;
// ===== 新增地址 =====
if (this.editing_address_index === '') {
url = this.host + '/users/addresses/create/';
method = 'post';
}
// ===== 编辑地址 =====
else {
url = this.host + '/users/addresses/' +
this.addresses[this.editing_address_index].id + '/';
method = 'put';
}
// title 默认等于 receiver
this.form_address.title = this.form_address.receiver;
axios({
url: url,
method: method,
data: this.form_address,
headers: {
'X-CSRFToken': getCookie('csrftoken')
}
}).then(res => {
if (res.data.code === '0') {
if (method === 'post') {
// 新增
this.addresses.unshift(res.data.address);
// ⭐ 如果这是第一个地址,前端同步设为默认
if (this.addresses.length === 1) {
this.default_address_id = res.data.address.id;
}
} else {
// 修改
this.$set(
this.addresses,
this.editing_address_index,
res.data.address
);
}
this.is_show_edit = false;
} else {
alert(res.data.errmsg || '操作失败');
}
});
},
// 删除地址
delete_address(index) {
axios.delete(
this.host + '/users/addresses/' + this.addresses[index].id + '/',
{
headers: {
'X-CSRFToken': getCookie('csrftoken')
}
}
).then(res => {
if (res.data.code === '0') {
this.addresses.splice(index, 1);
} else {
alert(res.data.errmsg || '删除失败');
}
});
},
// 设置默认地址
set_default(index) {
axios.put(
this.host + '/users/addresses/' + this.addresses[index].id + '/default/',
{},
{
headers: {
'X-CSRFToken': getCookie('csrftoken')
}
}
).then(res => {
if (res.data.code === '0') {
this.default_address_id = this.addresses[index].id;
} else {
alert(res.data.errmsg || '设置失败');
}
});
}
}
});
逻辑解析(编辑/删除 为例)
/users/addresses/<id>/ ——【编辑 / 删除 API】
path('addresses/<int:address_id>/', AddressUpdateView.as_view())
Vue 对应逻辑
修改地址
axios.put('/users/addresses/4/', form_address)
删除地址
axios.delete('/users/addresses/4/')
后端职责
class AddressUpdateView(LoginRequiredJSONMixin, View):
def put(self, request, address_id):
Address.objects.filter(id=address_id).update(...)
def delete(self, request, address_id):
Address.objects.filter(id=address_id).update(is_deleted=True)
测试:
http://127.0.0.1:8000/users/addresses/

用户修改密码
完整链路:
HTML(v-model)
↓
Vue 校验
↓
axios PUT /users/password/
↓
后端 check_password
↓
user.set_password
↓
logout
↓
前端跳转登录页
templates/users/user_center_pass.html
var vm = new Vue({
el: '#app',
delimiters: ['[[', ']]'],
data: {
host,
is_show_edit: false,
// 省市区数据
provinces: [],
cities: [],
districts: [],
// 地址列表
addresses: [],
default_address_id: null,
// 编辑状态
editing_address_index: '',
// 表单数据
form_address: {
title: '',
receiver: '',
province_id: '',
city_id: '',
district_id: '',
place: '',
mobile: '',
tel: '',
email: '',
}
},
mounted() {
// Django 注入的数据
this.addresses = JSON.parse(JSON.stringify(addresses));
this.default_address_id = default_address_id;
// 加载省份
this.get_provinces();
},
watch: {
// 省 -> 市
'form_address.province_id'(val) {
if (!val) {
this.cities = [];
this.districts = [];
return;
}
axios.get(this.host + '/areas/?area_id=' + val)
.then(res => {
if (res.data.code === '0') {
this.cities = res.data.sub_data;
this.districts = [];
}
});
},
// 市 -> 区
'form_address.city_id'(val) {
if (!val) {
this.districts = [];
return;
}
axios.get(this.host + '/areas/?area_id=' + val)
.then(res => {
if (res.data.code === '0') {
this.districts = res.data.sub_data;
}
});
}
},
methods: {
// 获取省份
get_provinces() {
axios.get(this.host + '/areas/')
.then(res => {
if (res.data.code === '0') {
this.provinces = res.data.province_list;
}
});
},
// 显示新增弹窗
show_add_site() {
this.is_show_edit = true;
this.editing_address_index = '';
this.form_address = {
title: '',
receiver: '',
province_id: '',
city_id: '',
district_id: '',
place: '',
mobile: '',
tel: '',
email: '',
};
this.cities = [];
this.districts = [];
},
// 显示编辑弹窗
show_edit_site(index) {
this.is_show_edit = true;
this.editing_address_index = index;
// 深拷贝,避免污染原数据
this.form_address = JSON.parse(JSON.stringify(this.addresses[index]));
// 回显省市区
axios.get(this.host + '/areas/?area_id=' + this.form_address.province_id)
.then(res => {
this.cities = res.data.sub_data;
});
axios.get(this.host + '/areas/?area_id=' + this.form_address.city_id)
.then(res => {
this.districts = res.data.sub_data;
});
},
// 保存地址(新增 / 修改)
save_address() {
let url;
let method;
// ===== 新增地址 =====
if (this.editing_address_index === '') {
url = this.host + '/users/addresses/create/';
method = 'post';
}
// ===== 编辑地址 =====
else {
url = this.host + '/users/addresses/' +
this.addresses[this.editing_address_index].id + '/';
method = 'put';
}
// title 默认等于 receiver
this.form_address.title = this.form_address.receiver;
axios({
url: url,
method: method,
data: this.form_address,
headers: {
'X-CSRFToken': getCookie('csrftoken')
}
}).then(res => {
if (res.data.code === '0') {
if (method === 'post') {
// 新增
this.addresses.unshift(res.data.address);
// ⭐ 如果这是第一个地址,前端同步设为默认
if (this.addresses.length === 1) {
this.default_address_id = res.data.address.id;
}
} else {
// 修改
this.$set(
this.addresses,
this.editing_address_index,
res.data.address
);
}
this.is_show_edit = false;
} else {
alert(res.data.errmsg || '操作失败');
}
});
},
// 删除地址
delete_address(index) {
axios.delete(
this.host + '/users/addresses/' + this.addresses[index].id + '/',
{
headers: {
'X-CSRFToken': getCookie('csrftoken')
}
}
).then(res => {
if (res.data.code === '0') {
this.addresses.splice(index, 1);
} else {
alert(res.data.errmsg || '删除失败');
}
});
},
// 设置默认地址
set_default(index) {
axios.put(
this.host + '/users/addresses/' + this.addresses[index].id + '/default/',
{},
{
headers: {
'X-CSRFToken': getCookie('csrftoken')
}
}
).then(res => {
if (res.data.code === '0') {
this.default_address_id = this.addresses[index].id;
} else {
alert(res.data.errmsg || '设置失败');
}
});
}
}
});
static/js/user_center_pass.js
var vm = new Vue({
el: '#app',
// 修改Vue变量的读取语法,避免和django模板语法冲突
delimiters: ['[[', ']]'],
data: {
host: host,
old_pwd: '',
new_pwd: '',
new_cpwd: '',
error_opwd: false,
error_pwd: false,
error_cpwd: false
},
methods: {
// 检查旧密码
check_opwd(){
var re = /^[0-9A-Za-z]{8,20}$/;
if (re.test(this.old_pwd)) {
this.error_opwd = false;
} else {
this.error_opwd = true;
}
},
// 检查新密码
check_pwd(){
var re = /^[0-9A-Za-z]{8,20}$/;
if (re.test(this.new_pwd)) {
this.error_pwd = false;
} else {
this.error_pwd = true;
}
},
// 检查确认密码
check_cpwd: function () {
if (this.new_pwd != this.new_cpwd) {
this.error_cpwd = true;
} else {
this.error_cpwd = false;
}
},
// 提交修改密码
on_submit: function () {
this.check_opwd();
this.check_pwd();
this.check_cpwd();
if (this.error_opwd || this.error_pwd || this.error_cpwd) {
return;
}
axios.put(this.host + '/users/password/', {
old_password: this.old_pwd,
new_password: this.new_pwd
}, {
headers: {
'X-CSRFToken': getCookie('csrftoken')
}
}).then(res => {
if (res.data.code === '0') {
alert('密码修改成功,请重新登录');
window.location.href = '/users/login/';
} else {
alert(res.data.errmsg);
}
}).catch(err => {
console.error(err);
alert('请求失败,请查看控制台');
});
}
}
});
apps/users/urls.py
from .views import PasswordView
...
urlpatterns = [
...
# ===== 修改密码 =====
path("password/", PasswordView.as_view(), name="password"),
]
apps/users/views.py
# ============================================================
# 修改密码
# ============================================================
import json
from django.views import View
from django import http
from django.contrib.auth import logout
import re
from utils.mixins import LoginRequiredJSONMixin
from utils.response_code import RETCODE
class PasswordView(LoginRequiredJSONMixin, View):
"""
修改密码页面
GET /users/password/
"""
def get(self, request):
return render(request, "users/user_center_pass.html")
"""
PUT /users/password/
"""
def put(self, request):
try:
data = json.loads(request.body.decode())
except Exception:
return http.JsonResponse({
'code': RETCODE.PARAMERR,
'errmsg': '参数格式错误'
})
old_password = data.get('old_password')
new_password = data.get('new_password')
# 1️⃣ 参数校验
if not all([old_password, new_password]):
return http.JsonResponse({
'code': RETCODE.PARAMERR,
'errmsg': '参数不完整'
})
# 2️⃣ 校验新密码格式
if not re.match(r'^[0-9A-Za-z]{8,20}$', new_password):
return http.JsonResponse({
'code': RETCODE.PARAMERR,
'errmsg': '新密码格式不正确'
})
user = request.user
# 3️⃣ 校验旧密码
if not user.check_password(old_password):
return http.JsonResponse({
'code': RETCODE.PARAMERR,
'errmsg': '原密码错误'
})
# 4️⃣ 设置新密码(必须用 set_password)
user.set_password(new_password)
user.save()
# 5️⃣ 安全处理:退出登录
logout(request)
return http.JsonResponse({
'code': RETCODE.OK,
'errmsg': 'ok'
})
测试:


尝试新的登录即可
发布者:LJH,转发请注明出处:https://www.ljh.cool/44495.html