购物车实现
后端功能点
| 功能 | 接口含义 |
|---|---|
| 添加购物车 | SKU + 数量 |
| 查询购物车 | 当前用户购物车列表 |
| 修改购物车 | 改数量 / 勾选状态 |
| 删除购物车 | 删除指定 SKU |
| 全选 / 反选 | 修改所有 SKU 的 selected |
| 合并购物车 | 未登录 Cookie → 登录 Redis |
前端表现
商品详情页:右上角 “简易购物车数量”
购物车页面:
- 数量加减
- 勾选 / 全选
- 删除
- 价格实时变化
部署项目
第一步:创建 carts 子应用目录
在你的项目根目录(meiduo_mall)执行(也可以用 PyCharm 直接建文件,效果一样。):
mkdir -p apps/carts
touch apps/carts/__init__.py apps/carts/apps.py apps/carts/urls.py apps/carts/views.py apps/carts/utils.py
第二步:注册 carts app
编辑 apps/carts/apps.py:
from django.apps import AppConfig
class CartsConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'apps.carts'
然后打开 meiduo_mall/settings.py,把它加进 INSTALLED_APPS:
INSTALLED_APPS = [
# ...
'apps.carts',
]
第三步:配置 Redis(Django-Redis,用已有的配置)
# -------- Redis 缓存配置 --------
CACHES = {
"default": { # 0 号库
"BACKEND": "django_redis.cache.RedisCache",
"LOCATION": "redis://127.0.0.1:6379/0",
"OPTIONS": {
"CLIENT_CLASS": "django_redis.client.DefaultClient",
"SERIALIZER": "django_redis.serializers.pickle.PickleSerializer",
}
},
"session": { # 1 号库(专门存 Session)
"BACKEND": "django_redis.cache.RedisCache",
"LOCATION": "redis://127.0.0.1:6379/1",
"OPTIONS": {
"CLIENT_CLASS": "django_redis.client.DefaultClient",
}
},
"verify_codes": { # 2 号库(验证码专用)
"BACKEND": "django_redis.cache.RedisCache",
"LOCATION": "redis://127.0.0.1:6379/2",
"OPTIONS": {
"CLIENT_CLASS": "django_redis.client.DefaultClient",
}
},
"carts": { # 新增 3 号库
"BACKEND": "django_redis.cache.RedisCache",
"LOCATION": "redis://127.0.0.1:6379/3",
"OPTIONS": {
"CLIENT_CLASS": "django_redis.client.DefaultClient",
}
}
}
第四步:写 urls
apps/carts/urls.py
from django.urls import path
from . import views
app_name = 'carts'
urlpatterns = [
# 购物车页面(HTML)
path('', views.CartView.as_view(), name='info'),
# 购物车操作(JSON)
path('', views.CartView.as_view(), name='carts'),
path('selection/', views.CartSelectAllView.as_view(), name='selection'),
path('simple/', views.CartSimpleView.as_view(), name='simple'),
]
主路由 meiduo_mall/urls.py 加 include
from django.urls import path, include
urlpatterns = [
# ...
# 购物车模块
path('carts/', include(('apps.carts.urls', 'carts'), namespace='carts')),
]
第五步:写 utils(Cookie 方案:signing + JSON)
apps/carts/utils.py
import json
from django.core import signing
from django.conf import settings
from django_redis import get_redis_connection
CARTS_COOKIE_NAME = "carts"
CARTS_COOKIE_SALT = "meiduo.carts"
CARTS_COOKIE_MAX_AGE = 14 * 24 * 3600 # 14 天
def _loads_cookie_carts(request):
"""
返回统一格式:
{ sku_id(int): {"count": int, "selected": bool}, ... }
"""
cookie_str = request.COOKIES.get(CARTS_COOKIE_NAME)
if not cookie_str:
return {}
try:
data = signing.loads(cookie_str, salt=CARTS_COOKIE_SALT)
# data should be dict like {"1":{"count":2,"selected":true}}
carts = {}
for k, v in data.items():
sku_id = int(k)
carts[sku_id] = {
"count": int(v.get("count", 1)),
"selected": bool(v.get("selected", True)),
}
return carts
except Exception:
# 验签失败/格式错误 => 当作空购物车(也可以选择清 cookie)
return {}
def _dumps_cookie_carts(carts: dict) -> str:
"""
carts: { sku_id(int): {"count": int, "selected": bool} }
存成可验签字符串
"""
data = {str(k): {"count": int(v["count"]), "selected": bool(v["selected"])} for k, v in carts.items()}
return signing.dumps(data, salt=CARTS_COOKIE_SALT, compress=True)
def save_cookie_carts_to_response(response, carts: dict):
cookie_value = _dumps_cookie_carts(carts)
response.set_cookie(
CARTS_COOKIE_NAME,
cookie_value,
max_age=CARTS_COOKIE_MAX_AGE,
httponly=True,
samesite="Lax",
secure=getattr(settings, "SESSION_COOKIE_SECURE", False),
)
def delete_cookie_carts(response):
response.delete_cookie(CARTS_COOKIE_NAME)
def merge_cookie_to_redis(request, user, response):
"""
登录后合并 cookie -> redis
规则(工程化版):
- count 以 cookie 为准覆盖 redis(课程一致)
- selected 以 cookie 为准
"""
carts = _loads_cookie_carts(request)
if not carts:
return response
redis_conn = get_redis_connection("carts")
cart_key = f"carts_{user.id}"
selected_key = f"selected_{user.id}"
mapping = {str(sku_id): int(item["count"]) for sku_id, item in carts.items()}
selected_ids = [str(sku_id) for sku_id, item in carts.items() if item["selected"]]
unselected_ids = [str(sku_id) for sku_id, item in carts.items() if not item["selected"]]
pl = redis_conn.pipeline()
# 覆盖/写入 count
pl.hset(cart_key, mapping=mapping)
# 选中状态以 cookie 为准:选中加到 set,未选中从 set 移除
if selected_ids:
pl.sadd(selected_key, *selected_ids)
if unselected_ids:
pl.srem(selected_key, *unselected_ids)
pl.execute()
delete_cookie_carts(response)
return response
def get_carts_from_storage(request):
"""
统一读购物车(redis or cookie)
返回:{ sku_id(int): {"count": int, "selected": bool}, ...}
"""
user = request.user
if user.is_authenticated:
redis_conn = get_redis_connection("carts")
cart_key = f"carts_{user.id}"
selected_key = f"selected_{user.id}"
# bytes -> str/int
sku_id_count = redis_conn.hgetall(cart_key) # {b'1': b'2'}
selected_ids = redis_conn.smembers(selected_key) # {b'1', b'3'}
selected_set = {int(x) for x in selected_ids}
carts = {}
for b_sku_id, b_count in sku_id_count.items():
sku_id = int(b_sku_id)
count = int(b_count)
carts[sku_id] = {"count": count, "selected": sku_id in selected_set}
return carts
return _loads_cookie_carts(request)
第六步:写 views(核心:增删改查 + 全选 + 简易购物车)
apps/carts/views.py
import json
from decimal import Decimal
from django import http
from django.shortcuts import render
from django.views import View
from django_redis import get_redis_connection
from apps.goods.models import SKU
from utils.response_code import RETCODE
from apps.carts.utils import (
get_carts_from_storage,
save_cookie_carts_to_response,
get_carts_from_storage,
)
from apps.carts.utils import get_carts_from_storage, save_cookie_carts_to_response, delete_cookie_carts
def _to_int(value, default=None):
try:
return int(value)
except Exception:
return default
def _validate_count(count: int) -> int:
# 与你前端一致:最大 5
if count < 1:
return 1
if count > 5:
return 5
return count
def _sku_to_cart_dict(sku: SKU, count: int, selected: bool):
# 注意:前端 render_carts 里把 'True'/'False' 转 bool,所以这里保持字符串更省事
return {
"id": sku.id,
"name": sku.name,
"count": count,
"selected": selected,
"default_image_url": sku.default_image.url if getattr(sku, "default_image", None) else "",
"price": str(sku.price),
"amount": str((sku.price * Decimal(count))),
}
class CartView(View):
"""
POST /carts/ 添加
GET /carts/ 展示购物车页
PUT /carts/ 修改 count/selected
DELETE /carts/ 删除 sku
"""
def post(self, request):
try:
data = json.loads(request.body.decode())
except Exception:
return http.JsonResponse({"code": RETCODE.PARAMERR, "errmsg": "参数格式错误"})
sku_id = _to_int(data.get("sku_id"))
count = _to_int(data.get("count"))
selected = data.get("selected", True)
if sku_id is None or count is None:
return http.JsonResponse({"code": RETCODE.PARAMERR, "errmsg": "参数不齐"})
count = _validate_count(count)
selected = bool(selected)
try:
SKU.objects.get(pk=sku_id)
except SKU.DoesNotExist:
return http.JsonResponse({"code": RETCODE.NODATAERR, "errmsg": "商品不存在"})
user = request.user
if user.is_authenticated:
redis_conn = get_redis_connection("carts")
cart_key = f"carts_{user.id}"
selected_key = f"selected_{user.id}"
pl = redis_conn.pipeline()
pl.hincrby(cart_key, str(sku_id), count)
if selected:
pl.sadd(selected_key, str(sku_id))
else:
pl.srem(selected_key, str(sku_id))
pl.execute()
return http.JsonResponse({"code": RETCODE.OK, "errmsg": "ok"})
# 未登录:cookie
carts = get_carts_from_storage(request)
if sku_id in carts:
carts[sku_id]["count"] = _validate_count(carts[sku_id]["count"] + count)
# selected:如果本次添加带 selected,就以本次为准;否则保留原值也行
carts[sku_id]["selected"] = selected
else:
carts[sku_id] = {"count": count, "selected": selected}
resp = http.JsonResponse({"code": RETCODE.OK, "errmsg": "ok"})
save_cookie_carts_to_response(resp, carts)
return resp
def get(self, request):
carts = get_carts_from_storage(request) # {sku_id: {count, selected}}
ids = list(carts.keys())
skus = SKU.objects.filter(id__in=ids)
sku_list = []
for sku in skus:
item = carts.get(sku.id)
sku_list.append(
{
"id": sku.id,
"name": sku.name,
"count": item["count"],
"selected": "True" if item["selected"] else "False",
"default_image_url": sku.default_image.url if getattr(sku, "default_image", None) else "",
"price": str(sku.price),
"amount": str(sku.price * Decimal(item["count"])),
}
)
return render(request, "cart.html", context={"cart_skus": sku_list})
def put(self, request):
try:
data = json.loads(request.body.decode())
except Exception:
return http.JsonResponse({"code": RETCODE.PARAMERR, "errmsg": "参数格式错误"})
sku_id = _to_int(data.get("sku_id"))
count = _to_int(data.get("count"))
selected = data.get("selected", True)
if sku_id is None or count is None:
return http.JsonResponse({"code": RETCODE.PARAMERR, "errmsg": "参数不齐"})
count = _validate_count(count)
selected = bool(selected)
try:
sku = SKU.objects.get(pk=sku_id)
except SKU.DoesNotExist:
return http.JsonResponse({"code": RETCODE.NODATAERR, "errmsg": "商品不存在"})
user = request.user
if user.is_authenticated:
redis_conn = get_redis_connection("carts")
cart_key = f"carts_{user.id}"
selected_key = f"selected_{user.id}"
pl = redis_conn.pipeline()
pl.hset(cart_key, str(sku_id), count)
if selected:
pl.sadd(selected_key, str(sku_id))
else:
pl.srem(selected_key, str(sku_id))
pl.execute()
cart_sku = {
"id": sku_id,
"count": count,
"selected": selected,
"name": sku.name,
"default_image_url": sku.default_image.url if getattr(sku, "default_image", None) else "",
"price": str(sku.price),
"amount": str(sku.price * Decimal(count)),
}
return http.JsonResponse({"code": RETCODE.OK, "errmsg": "ok", "cart_sku": cart_sku})
# 未登录:cookie
carts = get_carts_from_storage(request)
if sku_id not in carts:
carts[sku_id] = {"count": count, "selected": selected}
else:
carts[sku_id]["count"] = count
carts[sku_id]["selected"] = selected
cart_sku = {
"id": sku_id,
"count": count,
"selected": selected,
"name": sku.name,
"default_image_url": sku.default_image.url if getattr(sku, "default_image", None) else "",
"price": str(sku.price),
"amount": str(sku.price * Decimal(count)),
}
resp = http.JsonResponse({"code": RETCODE.OK, "errmsg": "ok", "cart_sku": cart_sku})
save_cookie_carts_to_response(resp, carts)
return resp
def delete(self, request):
try:
data = json.loads(request.body.decode())
except Exception:
return http.JsonResponse({"code": RETCODE.PARAMERR, "errmsg": "参数格式错误"})
sku_id = _to_int(data.get("sku_id"))
if sku_id is None:
return http.JsonResponse({"code": RETCODE.PARAMERR, "errmsg": "参数不齐"})
user = request.user
if user.is_authenticated:
redis_conn = get_redis_connection("carts")
cart_key = f"carts_{user.id}"
selected_key = f"selected_{user.id}"
pl = redis_conn.pipeline()
pl.hdel(cart_key, str(sku_id))
pl.srem(selected_key, str(sku_id))
pl.execute()
return http.JsonResponse({"code": RETCODE.OK, "errmsg": "ok"})
carts = get_carts_from_storage(request)
if sku_id in carts:
del carts[sku_id]
resp = http.JsonResponse({"code": RETCODE.OK, "errmsg": "ok"})
save_cookie_carts_to_response(resp, carts)
return resp
class CartSelectAllView(View):
"""
PUT /carts/selection/
body: {"selected": true/false}
"""
def put(self, request):
try:
data = json.loads(request.body.decode())
except Exception:
return http.JsonResponse({"code": RETCODE.PARAMERR, "errmsg": "参数格式错误"})
selected = bool(data.get("selected", True))
user = request.user
if user.is_authenticated:
redis_conn = get_redis_connection("carts")
cart_key = f"carts_{user.id}"
selected_key = f"selected_{user.id}"
sku_ids = redis_conn.hkeys(cart_key) # [b'1', b'2']
sku_ids = [sid.decode() if isinstance(sid, (bytes, bytearray)) else str(sid) for sid in sku_ids]
pl = redis_conn.pipeline()
if selected:
if sku_ids:
pl.sadd(selected_key, *sku_ids)
else:
# 反选:清空 set(最快)
pl.delete(selected_key)
pl.execute()
return http.JsonResponse({"code": RETCODE.OK, "errmsg": "ok"})
# 未登录:cookie
carts = get_carts_from_storage(request)
for sku_id in list(carts.keys()):
carts[sku_id]["selected"] = selected
resp = http.JsonResponse({"code": RETCODE.OK, "errmsg": "ok"})
save_cookie_carts_to_response(resp, carts)
return resp
class CartSimpleView(View):
"""
GET /carts/simple/
用于“顶部简易购物车数量显示”
返回 total_count(不管是否勾选)
"""
def get(self, request):
carts = get_carts_from_storage(request)
total_count = sum(item["count"] for item in carts.values()) if carts else 0
return http.JsonResponse({"code": RETCODE.OK, "errmsg": "ok", "total_count": total_count})
第七步:把“登录合并购物车”挂进去(普通登录 + QQ 登录)
普通登录(apps/users/views.py)
你找到你项目登录视图里调用 login(request, user) 的位置,然后:
顶部 import
from apps.carts.utils import merge_cookie_to_redis
找到下面这部分代码,
login(request, user)
if next_url:
resp = redirect(next_url)
else:
resp = redirect(reverse("contents:index"))
resp.set_cookie("username", user.username, max_age=14 * 24 * 3600)
return resp
在 return 前合并
login(request, user)
if next_url:
resp = redirect(next_url)
else:
resp = redirect(reverse("contents:index"))
# 合并购物车
resp = merge_cookie_to_redis(request, user, resp)
resp.set_cookie("username", user.username, max_age=14 * 24 * 3600)
return resp
合并要在用户已登录的前提下做,所以放在 login() 之后。
QQ 登录(apps/oauth/views.py)
QQ 回调里一般会 login(request, user) 或创建用户后登录。
1、已绑定用户(get 方法)
找到下面这行代码:
# 已绑定:直接登录
user = qquser.user
response = redirect(reverse('contents:index'))
login(request, user)
response.set_cookie('username', user.username, max_age=14 * 24 * 3600)
return response
改成:
from apps.carts.utils import merge_cookie_to_redis
response = redirect(reverse('contents:index'))
login(request, user)
# 合并购物车
response = merge_cookie_to_redis(request, user, response)
response.set_cookie('username', user.username, max_age=14 * 24 * 3600)
return response
2、绑定后登录(post 方法)
现在是
# 登录 + cookie
login(request, user)
response = redirect(reverse('contents:index'))
response.set_cookie('username', user.username, max_age=14 * 24 * 3600)
return response
改成
from apps.carts.utils import merge_cookie_to_redis
response = redirect(reverse('contents:index'))
login(request, user)
# 合并购物车
response = merge_cookie_to_redis(request, user, response)
response.set_cookie('username', user.username, max_age=14 * 24 * 3600)
return response
第八步:前端“商品页面简单购物车”怎么用
核心购物车页面
templates/cart.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>
</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_btn fl" v-if="username">
欢迎您:<em>[[ username ]]</em>
<span>|</span>
<a href="{{ url('users:logout') }}">退出</a>
</div>
<div class="login_btn fl" v-else>
<a href="{{ url('users:login') }}">登录</a>
<span>|</span>
<a href="{{ url('users:register') }}">注册</a>
</div>
<div class="user_link fl">
<span>|</span>
<a href="{{ url('users:center') }}">用户中心</a>
<span>|</span>
<a href="{{ url('carts:carts') }}">我的购物车</a>
</div>
</div>
</div>
</div>
<!-- 搜索 -->
<div class="search_bar clearfix">
<a href="{{ url('contents:index') }}" 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" value="搜索">
</form>
</div>
</div>
<!-- 总数量 -->
<div class="total_count">全部商品<em>[[ total_count ]]</em>件</div>
<!-- 表头 -->
<ul class="cart_list_th clearfix">
<li class="col01">选择</li>
<li class="col02">商品图片</li>
<li class="col03">商品名称</li>
<li class="col04">商品单位</li>
<li class="col05">商品价格</li>
<li class="col06">数量</li>
<li class="col07">小计</li>
<li class="col08">操作</li>
</ul>
<!-- 商品列表 -->
<ul class="cart_list_td clearfix" v-for="(cart,index) in carts" :key="cart.id">
<li class="col01">
<input type="checkbox" v-model="cart.selected" @change="update_selected(index)">
</li>
<li class="col02">
<img :src="cart.default_image_url">
</li>
<li class="col03">[[ cart.name ]]</li>
<!-- 单位列(你没有单位字段就写固定值) -->
<li class="col04">部</li>
<li class="col05">[[ cart.price ]] 元</li>
<li class="col06">
<div class="num_add">
<a @click="on_minus(index)" class="minus fl">-</a>
<input v-model="cart.count" @blur="on_input(index)" type="text" class="num_show fl">
<a @click="on_add(index)" class="add fl">+</a>
</div>
</li>
<li class="col07">[[ cart.amount ]] 元</li>
<li class="col08">
<a @click="on_delete(index)">删除</a>
</li>
</ul>
<!-- 结算 -->
<ul class="settlements">
<li class="col01">
<input type="checkbox" v-model="selected_all" @change="on_selected_all">
</li>
<li class="col02">全选</li>
<li class="col03">
合计:¥ <em>[[ total_selected_amount ]]</em><br>
共 <b>[[ total_selected_count ]]</b> 件商品
</li>
<li class="col04">
<a href="javascript:;">去结算</a>
</li>
</ul>
</div>
<script>
var cart_skus = {{ cart_skus|safe }};
</script>
<script type="text/javascript" src="{{ static('js/common.js') }}"></script>
<script type="text/javascript" src="{{ static('js/cart.js') }}"></script>
</body>
</html>
static/js/cart.js
var vm = new Vue({
el: '#app',
// 修改 Vue 变量读取语法,避免和 Django 模板冲突
delimiters: ['[[', ']]'],
data: {
host,
carts: [],
total_count: 0,
total_selected_count: 0,
total_selected_amount: 0,
carts_tmp: [],
username: '',
},
computed: {
// 是否全选
selected_all() {
var selected = true;
for (var i = 0; i < this.carts.length; i++) {
if (this.carts[i].selected === false) {
selected = false;
break;
}
}
return selected;
},
},
mounted() {
// 初始化购物车数据
this.render_carts();
// 计算总数量
this.compute_total_count();
// 计算选中商品数量和金额
this.compute_total_selected_amount_count();
// 获取用户名
this.username = getCookie('username');
},
methods: {
// 初始化购物车数据
render_carts() {
// 深拷贝,避免引用问题
this.carts = JSON.parse(JSON.stringify(cart_skus));
// selected 字符串 → boolean
for (var i = 0; i < this.carts.length; i++) {
this.carts[i].selected = this.carts[i].selected === 'True';
}
// 保存一份初始快照(用于失败回滚)
this.carts_tmp = JSON.parse(JSON.stringify(this.carts));
},
// 计算商品总数量(不论是否选中)
compute_total_count() {
var total_count = 0;
for (var i = 0; i < this.carts.length; i++) {
total_count += parseInt(this.carts[i].count);
}
this.total_count = total_count;
},
// 计算选中商品的数量和总金额
compute_total_selected_amount_count() {
var amount = 0;
var total_count = 0;
for (var i = 0; i < this.carts.length; i++) {
if (this.carts[i].selected) {
amount += parseFloat(this.carts[i].price) * parseInt(this.carts[i].count);
total_count += parseInt(this.carts[i].count);
}
}
this.total_selected_amount = amount.toFixed(2);
this.total_selected_count = total_count;
},
// 减少数量
on_minus(index) {
if (this.carts[index].count > 1) {
this.update_count(index, this.carts[index].count - 1);
}
},
// 增加数量
on_add(index) {
var count = this.carts[index].count + 1;
if (count > 5) {
count = 5;
alert('超过商品数量上限');
}
this.update_count(index, count);
},
// 输入数量
on_input(index) {
var count = parseInt(this.carts[index].count);
if (isNaN(count) || count <= 0) {
count = 1;
} else if (count > 5) {
count = 5;
alert('超过商品数量上限');
}
this.update_count(index, count);
},
// 更新购物车数量 / 选中状态
update_count(index, count) {
var url = this.host + '/carts/';
axios.put(url, {
sku_id: this.carts[index].id,
count: count,
selected: this.carts[index].selected
}, {
headers: {
'X-CSRFToken': getCookie('csrftoken')
},
withCredentials: true,
responseType: 'json'
}).then(response => {
if (response.data.code === '0') {
// 用后端返回的数据替换当前项
Vue.set(this.carts, index, response.data.cart_sku);
// 重新计算
this.compute_total_selected_amount_count();
this.compute_total_count();
// 更新快照(深拷贝)
this.carts_tmp = JSON.parse(JSON.stringify(this.carts));
} else {
alert(response.data.errmsg);
this.carts[index] = JSON.parse(JSON.stringify(this.carts_tmp[index]));
}
}).catch(() => {
// 回滚
this.carts[index] = JSON.parse(JSON.stringify(this.carts_tmp[index]));
});
},
// 更新选中状态
update_selected(index) {
var url = this.host + '/carts/';
axios.put(url, {
sku_id: this.carts[index].id,
count: this.carts[index].count,
selected: this.carts[index].selected
}, {
headers: {
'X-CSRFToken': getCookie('csrftoken')
},
withCredentials: true,
responseType: 'json'
}).then(response => {
if (response.data.code === '0') {
this.carts[index].selected = response.data.cart_sku.selected;
this.compute_total_selected_amount_count();
this.compute_total_count();
} else {
alert(response.data.errmsg);
}
});
},
// 删除商品
on_delete(index) {
var url = this.host + '/carts/';
axios.delete(url, {
data: { sku_id: this.carts[index].id },
headers: {
'X-CSRFToken': getCookie('csrftoken')
},
withCredentials: true,
responseType: 'json'
}).then(response => {
if (response.data.code === '0') {
this.carts.splice(index, 1);
this.compute_total_selected_amount_count();
this.compute_total_count();
} else {
alert(response.data.errmsg);
}
});
},
// 全选 / 取消全选
on_selected_all() {
var selected = !this.selected_all;
axios.put(this.host + '/carts/selection/', {
selected: selected
}, {
headers: {
'X-CSRFToken': getCookie('csrftoken')
},
withCredentials: true,
responseType: 'json'
}).then(response => {
if (response.data.code === '0') {
for (var i = 0; i < this.carts.length; i++) {
this.carts[i].selected = selected;
}
this.compute_total_selected_amount_count();
this.compute_total_count();
} else {
alert(response.data.errmsg);
}
});
},
}
});
属于购物车主页面(Cart Page)
作用
- 展示购物车中所有商品
- 支持:
- 数量加减 / 输入
- 勾选 / 全选
- 删除商品
- 显示合计金额、合计数量
- 进入结算页
依赖前端资源
static/js/cart.js(核心逻辑)static/js/common.jsstatic/js/host.js- Vue + Axios
后端数据来源
GET /carts/- Django
CartView.get - 模板变量:var cart_skus = {{ cart_skus|safe }};
商品相关页面(“添加购物车”入口)
templates/detail.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>
<!-- ===== CSS ===== -->
<link rel="stylesheet" href="{{ static('css/reset.css') }}">
<link rel="stylesheet" href="{{ static('css/main.css') }}">
<!-- ===== JS ===== -->
<script src="{{ static('js/vue-2.5.16.js') }}"></script>
<script src="{{ static('js/axios-0.18.0.min.js') }}"></script>
<script src="{{ static('js/common.js') }}"></script>
<script src="{{ static('js/host.js') }}"></script>
</head>
<body>
<div id="app" v-cloak>
<!-- ================= 顶部 Header ================= -->
<div class="header_con">
<div class="header">
<div class="welcome fl">欢迎来到美多商城!</div>
<div class="fr">
<div class="login_btn fl" v-if="username">
欢迎您:<em>[[ username ]]</em>
<span>|</span>
<a href="/logout/" class="quit">退出</a>
</div>
<div class="login_btn fl" v-else>
<a href="/login/">登录</a>
<span>|</span>
<a href="/register/">注册</a>
</div>
<div class="user_link fl">
<span>|</span>
<a href="{{ url('users:center') }}">用户中心</a>
<span>|</span>
<a href="/carts/">我的购物车</a>
<span>|</span>
<a href="/orders/">我的订单</a>
</div>
</div>
</div>
</div>
<!-- ================= 搜索栏 ================= -->
<div class="search_bar clearfix">
<a href="/" 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" value="搜索">
</form>
</div>
<div class="guest_cart fr">
<a href="/carts/" class="cart_name fl">我的购物车</a>
<div class="goods_count fl">[[ cart_total_count ]]</div>
<ul class="cart_goods_show">
<li v-for="cart in carts">
<img :src="cart.default_image_url">
<h4>[[ cart.name ]]</h4>
<div>[[ cart.count ]]</div>
</li>
</ul>
</div>
</div>
<!-- ================= 分类导航 ================= -->
<div class="navbar_con">
{{ csrf_input }}
<div class="navbar">
<div class="sub_menu_con fl">
<h1 class="fl">商品分类</h1>
<ul class="sub_menu">
{% for cat1 in categories %}
<li>
<div class="level1">
{% for cat2 in cat1.subs %}
<a href="{{ url('goods:list', args=(cat2.id, 1)) }}">{{ cat2.name }}</a>
{% endfor %}
</div>
</li>
{% endfor %}
</ul>
</div>
</div>
</div>
<!-- ================= 面包屑 ================= -->
<div class="breadcrumb">
<a href="/">全部分类</a>
<span>></span>
<a href="#">{{ breadcrumb.cat1.name }}</a>
<span>></span>
<a href="#">{{ breadcrumb.cat2.name }}</a>
<span>></span>
<span>{{ breadcrumb.cat3.name }}</span>
</div>
<!-- ================= 商品详情 ================= -->
<div class="goods_detail_con clearfix">
<!-- 左侧图片(你自己的 sku_images 逻辑,保留) -->
<div class="goods_detail_pic fl">
{% if sku_images %}
<img src="{{ sku_images[0].image.url }}">
{% else %}
<img src="{{ static('images/no_image.png') }}">
{% endif %}
</div>
<!-- 右侧信息 -->
<div class="goods_detail_list fr">
<h3>{{ sku.name }}</h3>
<p>{{ sku.caption }}</p>
<div class="price_bar">
<span class="show_pirce">¥<em>{{ sku.price }}</em></span>
</div>
<div class="goods_num clearfix">
<div class="num_name fl">数 量:</div>
<div class="num_add fl">
<input v-model="sku_count" @blur="check_sku_count"
type="text" class="num_show fl">
<a @click="on_addition" class="add fr">+</a>
<a @click="on_minus" class="minus fr">-</a>
</div>
</div>
<!-- 规格选择 -->
{% for spec in specs %}
<div class="type_select">
<label>{{ spec.name }}:</label>
{% for option in spec.spec_options %}
{% if option.sku_id == sku.id %}
<a href="javascript:;" class="select">{{ option.value }}</a>
{% elif option.sku_id %}
<a href="{{ url('goods:detail', args=(option.sku_id,)) }}">{{ option.value }}</a>
{% else %}
<a href="javascript:;">{{ option.value }}</a>
{% endif %}
{% endfor %}
</div>
{% endfor %}
<div class="total">总价:<em>[[ sku_amount ]]元</em></div>
<div class="operate_btn">
<a href="javascript:;" class="add_cart" @click="add_cart">加入购物车</a>
</div>
</div>
</div>
<!-- ================= 商品详情 Tab ================= -->
<div class="main_wrap clearfix">
<div class="r_wrap fr clearfix">
<ul class="detail_tab clearfix">
<li @click="on_tab_content('detail')" :class="tab_content.detail?'active':''">商品详情</li>
<li @click="on_tab_content('pack')" :class="tab_content.pack?'active':''">规格与包装</li>
<li @click="on_tab_content('service')" :class="tab_content.service?'active':''">售后服务</li>
</ul>
<div class="tab_content" :class="tab_content.detail?'current':''">
<dl>
<dt>商品详情:</dt>
<dd>{{ sku.spu.desc_detail | safe }}</dd>
</dl>
</div>
<div class="tab_content" :class="tab_content.pack?'current':''">
<dl>
<dt>规格与包装:</dt>
<dd>{{ sku.spu.desc_pack | safe }}</dd>
</dl>
</div>
<div class="tab_content" :class="tab_content.service?'current':''">
<dl>
<dt>售后服务:</dt>
<dd>{{ sku.spu.desc_service | safe }}</dd>
</dl>
</div>
</div>
</div>
<!-- ================= Footer ================= -->
<div class="footer">
<p>CopyRight © 2016 北京美多商业股份有限公司</p>
</div>
</div>
<!-- ================= JS 变量初始化(必须) ================= -->
<script>
let category_id = "{{ sku.category.id }}";
let sku_price = "{{ sku.price }}";
let sku_id = "{{ sku.id }}";
</script>
<script src="{{ static('js/detail.js') }}"></script>
</body>
</html>
作用
- 用户点击「加入购物车」
- 向后端发送:
{
"sku_id": xxx,
"count": xxx
}
static/js/detail.js
var vm = new Vue({
el: '#app',
// 修改Vue变量的读取语法,避免和django模板语法冲突
delimiters: ['[[', ']]'],
data: {
host,
hots: [],
sku_id: sku_id,
sku_count: 1,
sku_price: sku_price,
sku_amount: 0,
category_id: category_id,
tab_content: {
detail: true,
pack: false,
comment: false,
service: false
},
comments: [],
score_classes: {
1: 'stars_one',
2: 'stars_two',
3: 'stars_three',
4: 'stars_four',
5: 'stars_five',
},
cart_total_count: 0, // 购物车总数量
carts: [], // 购物车数据,
username: '',
},
mounted(){
// 获取热销商品数据
this.get_hot_goods();
// 保存用户浏览记录
this.save_browse_histories();
// // 记录商品详情的访问量
// this.detail_visit();
// 获取购物车数据
this.get_carts();
// 获取商品评价信息
this.get_goods_comment();
this.username = getCookie('username');
},
watch: {
// 监听商品数量的变化
sku_count: {
handler(newValue){
this.sku_amount = (newValue * this.sku_price).toFixed(2);
},
immediate: true
}
},
methods: {
// 加数量
on_addition(){
if (this.sku_count < 5) {
this.sku_count++;
} else {
this.sku_count = 5;
alert('超过商品数量上限');
}
// this.sku_amount = (this.sku_count * this.sku_price).toFixed(2);
},
// 减数量
on_minus(){
if (this.sku_count > 1) {
this.sku_count--;
}
// this.sku_amount = (this.sku_count * this.sku_price).toFixed(2);
},
// 编辑商品数量
check_sku_count(){
if (this.sku_count > 5) {
this.sku_count = 5;
}
if (this.sku_count < 1) {
this.sku_count = 1;
}
// this.sku_amount = (this.sku_count * this.sku_price).toFixed(2);
},
// 控制页面标签页展示
on_tab_content(name){
this.tab_content = {
detail: false,
pack: false,
comment: false,
service: false
};
this.tab_content[name] = true;
},
// 获取热销商品数据
get_hot_goods(){
var url = this.host + '/hot/' + this.category_id + '/';
axios.get(url, {
responseType: 'json'
})
.then(response => {
this.hots = response.data.hot_skus;
for (var i = 0; i < this.hots.length; i++) {
this.hots[i].url = '/detail/' + this.hots[i].id + '/';
}
})
.catch(error => {
console.log(error.response);
})
},
// 保存用户浏览记录
save_browse_histories(){
if (this.sku_id) {
var url = this.host + '/browse_histories/';
axios.post(url, {
'sku_id': this.sku_id
}, {
headers: {
'X-CSRFToken': getCookie('csrftoken')
},
responseType: 'json'
})
.then(response => {
console.log(response.data);
})
.catch(error => {
console.log(error.response);
});
}
},
// 记录商品详情的访问量
// detail_visit(){
// if (this.category_id) {
// var url = this.hots + '/detail/visit/' + this.category_id + '/';
// axios.post(url, {}, {
// headers: {
// 'X-CSRFToken': getCookie('csrftoken')
// },
// responseType: 'json'
// })
// .then(response => {
// console.log(response.data);
// })
// .catch(error => {
// console.log(error.response);
// });
// }
// },
// 加入购物车
add_cart(){
var url = this.host + '/carts/';
axios.post(url, {
sku_id: parseInt(this.sku_id),
count: this.sku_count
}, {
headers: {
'X-CSRFToken': getCookie('csrftoken')
},
responseType: 'json',
withCredentials: true
})
.then(response => {
if (response.data.code == '0') {
alert('添加购物车成功');
this.cart_total_count += this.sku_count;
} else { // 参数错误
alert(response.data.errmsg);
}
})
.catch(error => {
console.log(error.response);
})
},
// 获取购物车数据
get_carts(){
var url = this.host + '/carts/simple/';
axios.get(url, {
responseType: 'json',
})
.then(response => {
this.carts = response.data.cart_skus;
this.cart_total_count = 0;
for (var i = 0; i < this.carts.length; i++) {
if (this.carts[i].name.length > 25) {
this.carts[i].name = this.carts[i].name.substring(0, 25) + '...';
}
this.cart_total_count += this.carts[i].count;
}
})
.catch(error => {
console.log(error.response);
})
},
// 获取商品评价信息
get_goods_comment(){
if (this.sku_id) {
var url = this.hots + '/comment/' + this.sku_id + '/';
axios.get(url, {
responseType: 'json'
})
.then(response => {
this.comments = response.data.goods_comment_list;
for (var i = 0; i < this.comments.length; i++) {
this.comments[i].score_class = this.score_classes[this.comments[i].score];
}
})
.catch(error => {
console.log(error.response);
});
}
},
}
});
公共头部(迷你购物车)
全站公共模板(Header + Mini Cart)
templates/base.html
作用
- 页面顶部:
- “我的购物车”
- 显示商品数量
- 鼠标移入展示迷你购物车
涉及 JS
static/js/base.jsstatic/js/common.js
<!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>{% block title %}{% endblock %}</title>
<link rel="stylesheet" type="text/css" href="{{ static('css/reset.css') }}">
<link rel="stylesheet" type="text/css" href="{{ static('css/main.css') }}">
<script src="{{ static('js/host.js') }}"></script>
<script src="{{ static('js/axios-0.18.0.min.js') }}"></script>
</head>
<body>
<div class="header_con">
<div class="header">
<div class="welcome fl">欢迎来到美多商城!</div>
<div class="fr">
{% if request.user.is_authenticated %}
<div class="login_btn fl">
欢迎您:<em>{{ request.user.username }}</em>
<span>|</span>
<a href="{{ url('users:logout') }}">退出</a>
</div>
{% else %}
<div class="login_btn fl">
<a href="{{ url('users:login') }}">登录</a>
<span>|</span>
<a href="{{ url('users:register') }}">注册</a>
</div>
{% endif %}
<div class="user_link fl">
<span>|</span>
<a href="{{ url('users:center') }}">用户中心</a>
<span>|</span>
<a href="{{ url('carts:carts') }}">我的购物车</a>
</div>
</div>
</div>
</div>
<div class="search_bar clearfix">
<a href="{{ url('contents:index') }}" class="logo fl">
<img src="{{ static('images/logo.png') }}">
</a>
<div class="guest_cart fr">
<a href="{{ url('carts:carts') }}" class="cart_name fl">我的购物车</a>
<div class="goods_count fl" id="cart-count">0</div>
</div>
</div>
{% block content %}{% endblock %}
<div class="footer">
<div class="foot_link">
<a href="#">关于我们</a>
<span>|</span>
<a href="#">联系我们</a>
<span>|</span>
<a href="#">招聘人才</a>
</div>
<p>CopyRight © 美多商城</p>
</div>
<script src="{{ static('js/common.js') }}"></script>
<script src="{{ static('js/base.js') }}"></script>
</body>
</html>
作用
- 页面顶部:
- “我的购物车”
- 显示商品数量
- 鼠标移入展示迷你购物车
涉及 JS
static/js/base.jsstatic/js/common.js
涉及接口
GET /carts/
static/js/list.js
var vm = new Vue({
el: '#app',
delimiters: ['[[', ']]'],
data: {
host,
cart_total_count: 0,
carts: [],
hots: [],
category_id: category_id,
username: '',
},
mounted(){
this.get_carts_simple();
this.get_hot_goods();
this.username = getCookie('username');
},
methods: {
// 迷你购物车(统一走 /carts/)
get_carts_simple(){
var url = this.host + '/carts/';
axios.get(url, {
withCredentials: true
})
.then(response => {
// 后端可能返回 HTML,需保护
if (!response.data || !response.data.cart_skus) {
this.cart_total_count = 0;
this.carts = [];
return;
}
this.carts = response.data.cart_skus.slice(0, 3);
this.cart_total_count = 0;
for (var i = 0; i < this.carts.length; i++) {
if (this.carts[i].name.length > 25) {
this.carts[i].name =
this.carts[i].name.substring(0, 25) + '...';
}
this.cart_total_count += this.carts[i].count;
}
})
.catch(() => {
this.cart_total_count = 0;
this.carts = [];
});
},
// 热销商品
get_hot_goods(){
var url = this.host + '/hot/' + this.category_id + '/';
axios.get(url)
.then(response => {
this.hots = response.data.hot_skus || [];
for (var i = 0; i < this.hots.length; i++) {
this.hots[i].url = '/goods/' + this.hots[i].id + '.html';
}
})
.catch(error => {
console.log(error.response);
});
},
}
});
static/js/base.js
/**
* base.js
* 顶部公共逻辑
* 说明:
* - base.html 已经用 Django 模板判断登录态
* - 这里不再使用 Vue,避免重复接管 DOM
* - 后续如需扩展(如顶部购物车数量),直接写普通 JS
*/
document.addEventListener('DOMContentLoaded', function () {
// 目前无需额外逻辑
// 预留给后续功能(如顶部购物车数量 / 消息提醒等)
});
static/js/common.js 这个保留原样即可
// 获取 cookie
function getCookie(name) {
var r = document.cookie.match("\\b" + name + "=([^;]*)\\b");
return r ? r[1] : undefined;
}
// 获取 URL 查询参数
function get_query_string(name) {
var reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)", "i");
var r = window.location.search.substr(1).match(reg);
if (r != null) {
return decodeURI(r[2]);
}
return null;
}
// 生成 UUID
function generateUUID() {
var d = new Date().getTime();
if (window.performance && typeof window.performance.now === "function") {
d += performance.now();
}
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) {
var r = (d + Math.random() * 16) % 16 | 0;
d = Math.floor(d / 16);
return (c === "x" ? r : (r & 0x3 | 0x8)).toString(16);
});
}
测试
python manage.py runserver 0.0.0.0:8000
打开商品详情页:http://127.0.0.1:8000/detail/1/
DevTools → Network → XHR
点「加入购物车」
请求应该是:
POST http://127.0.0.1:8000/carts/

Payload:
{
"sku_id": 1,
"count": 1
}

Response:
{
"code": "0"
}

验证后端是否真的收到

验证购物车页
http://127.0.0.1:8000/carts/

用户浏览历史纪录
功能目标
- 只记录登录用户
- 用户访问某个商品详情页时记录
- 记录是“商品级别”的(SKU)
- 在用户中心展示最近浏览(有顺序、有数量限制)

整体技术方案
用户访问商品详情页
↓
DetailView 中判断用户是否登录
↓
登录 → 把 sku_id 写入 Redis
↓
Redis 结构:history_<user_id> = [sku_id, sku_id, ...]
↓
用户中心接口从 Redis 取最近浏览
↓
查 SKU 表 → 返回商品信息 → 前端展示
Redis 数据结构设计
Key: history_<user_id>
Type: List
Value: [sku_id1, sku_id2, sku_id3, ...]
规则:
- 最新浏览的 SKU 放在最左边
- 去重
- 最多保存 5 条(或 10 条)
具体操作
第一步:在「商品详情页」记录浏览历史
apps/goods/views.py
class DetailView(View): 上面先导入 Redis
from django_redis import get_redis_connection
在 DetailView.get 里新增这一段代码
修改后的 DetailView
def get(self, request, sku_id):
try:
sku = SKU.objects.get(id=sku_id)
except SKU.DoesNotExist:
return render(request, '404.html')
# 最近浏览片段代码
user = request.user
if user.is_authenticated:
redis_conn = get_redis_connection("default")
history_key = f"history_{user.id}"
redis_conn.lrem(history_key, 0, sku.id)
redis_conn.lpush(history_key, sku.id)
redis_conn.ltrim(history_key, 0, 4)
categories = get_categories()
breadcrumb = get_breadcrumb(sku.category)
...
第二步:在用户中心读取最近浏览
在 UserCenterInfoView 中:
- 从 Redis 取 sku_id 列表
- 查 SKU
- 传给模板
修改apps/users/views.py
修改UserCenterInfoView
在 get() 里加这段逻辑
def get(self, request):
user = request.user
# ============================
# 最近浏览
# ============================
from django_redis import get_redis_connection
redis_conn = get_redis_connection("default")
history_key = f"history_{user.id}"
from apps.goods.models import SKU
sku_ids = redis_conn.lrange(history_key, 0, -1)
skus = []
if sku_ids:
sku_ids = [int(sku_id) for sku_id in sku_ids]
skus = SKU.objects.filter(id__in=sku_ids)
# ⚠️ 保证顺序
skus = sorted(
skus,
key=lambda x: sku_ids.index(x.id)
)
context = {
'username': user.username,
'mobile': user.mobile,
'email': user.email,
'email_active': user.email_active,
'history_skus': skus, # ⭐ 新增
}
return render(request, "users/user_center_info.html", context)
第三步:模板中展示最近浏览
templates/users/user_center_info.html
需要改「最近浏览」这一块
原位置
<h3 class="common_title2">最近浏览</h3>
<div class="has_view_list">
<ul class="goods_type_list clearfix">
<!-- 这里原来是写死的 li -->
</ul>
</div>
完整改造后
<h3 class="common_title2">最近浏览</h3>
<div class="has_view_list">
{% if history_skus %}
<ul class="goods_type_list clearfix">
{% for sku in history_skus %}
<li>
<a href="/detail/{{ sku.id }}/">
<img src="{{ sku.default_image.url }}" alt="{{ sku.name }}">
</a>
<h4>
<a href="/detail/{{ sku.id }}/">{{ sku.name }}</a>
</h4>
<div class="operate">
<span class="price">¥{{ sku.price }}</span>
<span class="unit">台</span>
<a href="javascript:;" class="add_goods" title="加入购物车"></a>
</div>
</li>
{% endfor %}
</ul>
{% else %}
<div class="no_view">
<p style="padding:20px;color:#999;">暂无浏览记录,快去逛逛吧~</p>
</div>
{% endif %}
</div>
修改后的完整页面代码:
<!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/common.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>
let username = "{{ username }}";
let mobile = "{{ mobile }}";
let email = "{{ email }}";
let email_active = "{{ email_active }}";
</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>[[ username ]]</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" class="active">· 个人信息</a></li>
<li><a href="../../static/user_center_order.html">· 全部订单</a></li>
<li><a href="{{ url('users:addresses') }}">· 收货地址</a></li>
<li><a href="user_center_pass.html">· 修改密码</a></li>
</ul>
</div>
<div class="right_content clearfix">
<div class="info_con clearfix">
<h3 class="common_title2">基本信息</h3>
<ul class="user_info_list">
<li><span>用户名:</span>[[ username ]]</li>
<li><span>联系方式:</span>[[ mobile ]]</li>
<li>
<span>Email:</span>
<!-- 未设置邮箱 or 正在编辑 -->
<div v-if="set_email">
<input
type="text"
name="email"
class="email"
v-model="email"
@blur="check_email"
>
<input
type="button"
value="保 存"
@click="save_email"
>
<input
type="reset"
value="取 消"
@click="cancel_email"
>
<div v-show="error_email" class="error_email_tip">
[[ error_email_message ]]
</div>
</div>
<!-- 已设置邮箱 -->
<div v-else>
<input
type="text"
class="email"
v-model="email"
readonly
>
<div v-if="email_active">
已验证
</div>
<div v-else>
待验证
<input
type="button"
:disabled="send_email_btn_disabled"
:value="send_email_tip"
@click="save_email"
>
</div>
</div>
</li>
</ul>
</div>
<h3 class="common_title2">最近浏览</h3>
<div class="has_view_list">
{% if history_skus %}
<ul class="goods_type_list clearfix">
{% for sku in history_skus %}
<li>
<a href="/detail/{{ sku.id }}/">
<img src="{{ sku.default_image.url }}" alt="{{ sku.name }}">
</a>
<h4>
<a href="/detail/{{ sku.id }}/">{{ sku.name }}</a>
</h4>
<div class="operate">
<span class="price">¥{{ sku.price }}</span>
<span class="unit">台</span>
<a href="javascript:;" class="add_goods" title="加入购物车"></a>
</div>
</li>
{% endfor %}
</ul>
{% else %}
<div class="no_view">
<p style="padding:20px;color:#999;">暂无浏览记录,快去逛逛吧~</p>
</div>
{% endif %}
</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>
<script type="text/javascript" src="../../static/js/user_center_info.js"></script>
</body>
</html>
登录后先浏览部分商品,然后进入页面查看:
http://127.0.0.1:8000/users/center/

通过 redis 查看 0 号库的存储情况

发布者:LJH,转发请注明出处:https://www.ljh.cool/44718.html