美多商城项目-07

购物车实现

后端功能点

功能接口含义
添加购物车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.js
  • static/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>&gt;</span>
    <a href="#">{{ breadcrumb.cat1.name }}</a>
    <span>&gt;</span>
    <a href="#">{{ breadcrumb.cat2.name }}</a>
    <span>&gt;</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.js
  • static/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.js
  • static/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/
美多商城项目-07

Payload:

{
  "sku_id": 1,
  "count": 1
}
美多商城项目-07

Response:

{
  "code": "0"
}
美多商城项目-07

验证后端是否真的收到

美多商城项目-07

验证购物车页

http://127.0.0.1:8000/carts/
美多商城项目-07

用户浏览历史纪录

功能目标

  • 只记录登录用户
  • 用户访问某个商品详情页时记录
  • 记录是“商品级别”的(SKU)
  • 在用户中心展示最近浏览(有顺序、有数量限制)
美多商城项目-07

整体技术方案

用户访问商品详情页
        ↓
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/

美多商城项目-07

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

美多商城项目-07

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

(0)
LJH的头像LJH
上一篇 2026年1月18日 下午7:09
下一篇 2023年6月8日 下午6:36

相关推荐

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注