day83:luffy:添加购物车&导航栏购物车数字显示&购物车页面展示
目录
1.添加购物车+验证登录状态
1.添加购物车的整体思想
用户在课程详情页面点击了加入购物车:
拿到当前课程的课程id 到数据库把课程id所对应的信息(需要在购物车显示的)加工成一个字典,然后json序列化成字符串保存到redis中
如何实现点击添加购物车,将购物车数据添加到redis中??????
2.添加购物车-后端接口
1.创建一个cart应用,并配置INSTALLAPP
python3 ../../ manage.py startapp cart
2.总路由中添加cart
# lyapi/urls.py path('cart/', include("cart.urls") ),
3.cart/urls.py
from django.urls import path,re_path from . import views urlpatterns = [ path('add_cart/', views.AddCartView.as_view({'post':'add'})) ]
4.cart/views.py
from rest_framework.viewsets import Viewset from django_redis import get_redis_connection from course import models from rest_framework.response import Response class AddCartView(ViewSet): def add(self,request): course_id = request.data.get('course_id') user_id = 1 # 先把用户id写死 # 去redis里存数据 conn = get_redis_connection('cart') # 校验一下课程id是否合法 try: models.Course.objects.get(id=course_id) except: return Response({'msg':'课程不存在'},status=400) '''选择用集合的数据类型去存储''' conn.sadd('cart_%s' % user_id,course_id) # vheader右方的购物车小红数字显示 cart_length = conn.scard('cart_%s' % user_id) # 获取商品数量 return Response({'msg':'添加成功','cart_length',cart_length})
5.单独给购物车使用一个redis库
# dev.py CACHES = { ...... "cart":{ "BACKEND": "django_redis.cache.RedisCache", "LOCATION": "redis://127.0.0.1:6379/3", "OPTIONS": { "CLIENT_CLASS": "django_redis.client.DefaultClient", }, } }
3.添加购物车-前端
前端点击添加购物车,向后端发送请求
<!-- html --> <div class="add-cart" @click="addCart"><img src="/static/img/cart-yellow.svg" alt="">加入购物车</div>
注意:添加购物车要验证用户是否已经登录
所以现在我们需要将两个请求变为同步请求,让token验证之后再进行别的操作
异步改成同步还是比较麻烦的,所以我们直接将验证token的操作写在addCart添加购物车方法里
// js addCart(){ // 获取前端存储的token值 let token = localStorage.token || sessionStorage.token; // 如果token值存在 if (token){ // 验证token this.$axios.post(`${this.$settings.Host}/users/verify/`,{ token:token, }).then((res)=>{ // 验证通过,可以添加购物车 this.$axios.post(`${this.$settings.Host}/cart/add_cart/`,{ // 获取课程id course_id:this.course_id, }).then((res)=>{ // 添加购物车成功,打印添加成功的信息 this.$message.success(res.data.msg); }) // 验证没有通过(token错误或者token过期) 提示用户让用户去登录 }).catch((error)=>{ this.$confirm('您还没有登录!!!?', '31s', { confirmButtonText: '去登录', cancelButtonText: '取消', type: 'warning' }).then(() => { this.$router.push('/user/login'); }) // 将过期的token清理掉 sessionStorage.removeItem('token'); sessionStorage.removeItem('username'); sessionStorage.removeItem('id'); localStorage.removeItem('token'); localStorage.removeItem('username'); localStorage.removeItem('id'); }) } // token获取不到 else { this.$confirm('您还没有登录!!!?', '31s', { confirmButtonText: '去登录', cancelButtonText: '取消', type: 'warning' }).then(() => { this.$router.push('/user/login'); }) } },
我们在后端已经将购物车的长度返回了,在前端我们就可以拿到购物车的长度
// Detail.vue this.$axios.post(`${this.$settings.Host}/cart/add_cart/`,{ // 获取课程id course_id:this.course_id, }).then((res)=>{ // 添加购物车成功,打印添加成功的信息 this.$message.success(res.data.msg); // 获取到后端发送过来的购物车长度 this.cart_length = res.data.cart_length })
第一种思路:vheader组件是detail组件的子组件,我们可以通过vue的父子传值来实现。
那问题就来了
如果我们访问实战课页面 也就是/course,在course组件是不也要显示那个购物车小红圆圈?
但是在course组件我们根本就没有去获取购物车的长度。所以红圆圈数字根本显示不出来。所以父子传值这个思路行不通。
3.Vuex
因为对于一些数据,需要在多个组件中即时共享,所以根据上述的问题,我们引出vuex
1.安装Vuex
npm install -S vuex
2.把vuex注册到vue中
在src目录下创建store目录,并在store目录下创建一个index.js文件,index.js文件代码
// store/index.js import Vue from 'vue' import Vuex from 'vuex' Vue.use(Vuex) export default new Vuex.Store({ state: { // 数据仓库,类似vue里面的data cart_length: 0 // 购物车数据 }, mutations: { // 数据操作方法,类似vue里面的methods add_cart (state, cart_length) { state.cart_length = cart_length; // 修改购物车的商品总数 } } })
3.挂载store对象
把上面index.js中创建的store对象注册到main.js的vue中。
// main.js import Vue from 'vue' import App from './App' import router from './router' import store from './store'; // 引入 new Vue({ el: '#app', router, store, // 挂载 components: { App }, template: '<App/>' })
4.Vheader组件读取store的数据(读取购物车长度)
在Vheader.vue头部组件中,直接就可以读取store里面的数据
<router-link to="/"> <b>{{$store.state.cart_length}}</b> <img src="@/assets/shopcart.png" alt=""> <span>购物车 </span> </router-link>
5.Detail组件修改store的数据(修改购物车长度)
当用户点击添加购物车时,触发addCart中的post方法,将购物车的长度进行修改
this.$axios.post(`${this.$settings.Host}/cart/add_cart/`,{ course_id:this.course_id, }).then((res)=>{ this.$message.success(res.data.msg); // 从后端获取到的购物车长度不存在当前组件的数据属性中了,而是存到vuex中 this.$store.commit('add_cart', res.data.cart_length) ; // commit用来触发mutation中声明的方法 })
6.关于页面刷新,vuex数据丢失问题
解决方式:用户点击刷新时,我们可以监听用户刷新的这个动作,可以在刷新之前对页面做一些动作。
当点击刷新时,我们先把数据存到sessionStorage或localStorage中,
页面刷新完成之后,再把数据取回来放到vuex中。这样的话就可以做到页面刷新了,数据也没有丢。
1.点击刷新,将数据存到sessionStorage中
// app.vue <script> export default { name: 'App', created() { // 页面刷新之前把cart_length数据存到了sessionStorage中 window.addEventListener('beforeunload',()=>{ console.log('页面要刷新啦!!!,赶紧保存数据!!!!'); sessionStorage.setItem('cart_length',this.$store.state.cart_length); }) } } </script>
2..页面刷新完成之后,将数据从sessionStorage取出来放到vuex中
// vheader.vue created() { if (this.$store.state.cart_length === 0) { let cart_length = sessionStorage.getItem('cart_length'); this.$store.commit('add_cart', cart_length); } },
7.关于redis的异常捕获
为了保证系统的日志记录可以跟进redis部分的,我们还可以在之前自定义异常处理中增加关于 redis的异常捕获
# utils/exceptions.py from rest_framework.views import exception_handler from django.db import DatabaseError from rest_framework.response import Response from rest_framework import status from redis import RedisError # 引入redis异常 import logging logger = logging.getLogger('django') def custom_exception_handler(exc, context): """ 自定义异常处理 :param exc: 异常类 :param context: 抛出异常的上下文 :return: Response响应对象 """ # 调用drf框架原生的异常处理方法 response = exception_handler(exc, context) if response is None: view = context['view'] # 错误出现的那个函数或者方法 if isinstance(exc, DatabaseError) or isinstance(exc, RedisError): # 数据库异常/redis异常 logger.error('[%s] %s' % (view, exc)) response = Response({'message': '服务器内部错误'}, status=status.HTTP_507_INSUFFICIENT_STORAGE) return response
4.购物车页面展示-后端接口
1.添加购物车-课程有效期
2.添加购物车时将有效期也存到redis中:之前的redis数据存储结构是集合,但是现在集合已经满足不了我们的需求了。要使用哈希数据类型存储。
哈希数据类型结构如下所示:
''' user_id:{ course_id:expire, course_id:expire, } '''
# cart/views.py class AddCartView(ViewSet): def add(self,request): ...... expire = 0 # 有效期:表示永久有效 '''存用户对应的课程id和有效期''' conn.hset('cart_%s' % user_id,course_id,expire) '''存放用户的购物车长度''' cart_length = conn.hlen('cart_%s' % user_id) ......
在上面的代码中,我们可以看到一共建立了两次conn连接,这样并不是很好,所以我们借助一个redis的管道pipe
pipe = conn.pipeline() # 创建管道 pipe.multi() # 将下面两个指令放到管道里面 '''存用户对应的课程id和有效期''' pipe.hset('cart_%s' % user_id,course_id,expire) '''存放用户的购物车长度''' cart_length = pipe.hlen('cart_%s' % user_id) pipe.execute() # 执行上面两条指令
2.购物车列表-后端接口
class AddCartView(ViewSet): def cart_list(self,request): user_id = 1 # 用户id先写死 conn = get_redis_connection('cart') # 获取cart对应的redis库对象 # 将当前用户所对应的课程id从redis中取出来 ret = conn.hgetall('cart_%s' % user_id) # 封装成了字典{课程id,有效期},dict {b'1': b'0', b'2': b'0'} cart_data_list = [] try: for cid, eid in ret.items():# cid:课程id eid:有效期 '''redis中存的是字节 所以要解码''' course_id = cid.decode('utf-8') expire_id = eid.decode('utf-8') course_obj = models.Course.objects.get(id=course_id) ''' 前端所需要的购物车数据包括 1.课程名称 2.课程封面图 3.课程价格 4.课程有效期 so 我们自己创建一个数据结构去存储前端所需要的内容 ''' cart_data_list.append({ 'name':course_obj.name, 'course_img':contains.SERVER_ADDR + course_obj.course_img.url , # 图片路径是相对路径,我们将其变为绝对路径 'price':course_obj.price, 'expire_id':expire_id }) except Exception: logger.error('获取购物车数据失败') return Response({'msg':'后台数据库出问题了,请联系管理员'},status=status.HTTP_507_INSUFFICIENT_STORAGE) # 将数据响应给前端 return Response({'msg':'xxx','cart_data_list':cart_data_list})
5.购物车页面展示-前端
1.购物车前端的初始界面
...
2.将cart组件注册到路由上
import Vue from 'vue' import Cart from '@/components/Cart' Vue.use(Router) export default new Router({ mode:'history', routes: [ { path: '/cart/', component: Cart }, ] })
3.关于cart组件和cartitem组件
在购物车页面中,整个购物车是一个组件(cart组件),然后要展示的每条购物车数据又是一个子组件(cartitem组件)
<!-- cart.vue html部分 --> <div class="cart_course_list"> <CartItem v-for="(value,index) in cart_data_list" :key="index" :cart="value"></CartItem> <!-- 001 :cart 父组件往子组件传值 --> </div>
// cart.vue js部分 <script> import CartItem from "./common/CartItem" export default { name: "Cart", data(){ return { cart_data_list:[], } }, methods:{ }, created() { let token = sessionStorage.token || localStorage.token; if (token){ this.$axios.get(`${this.$settings.Host}/cart/add_cart/`) // 获取购物车数据 .then((res)=>{ this.cart_data_list = res.data.cart_data_list }) .catch((error)=>{ this.$message.error(error.response.data.msg); }) }else { this.$router.push('/user/login'); } }, components:{ CartItem, } } </script>
父组件拿着自己的值 cart_data_list 传递给每个子组件进行渲染(父组件往子组件传值)
// cartitem.vue <template> <div class="cart_item"> <div class="cart_column column_1"> <el-checkbox class="my_el_checkbox" v-model="checked"></el-checkbox> </div> <div class="cart_column column_2"> <img :src="cart.course_img" alt=""> <span><router-link to="/course/detail/1">{{cart.name}}</router-link></span> </div> <div class="cart_column column_3"> <el-select v-model="cart.expire_id" size="mini" placeholder="请选择购买有效期" class="my_el_select"> <el-option label="1个月有效" value="30" key="30"></el-option> <el-option label="2个月有效" value="60" key="60"></el-option> <el-option label="3个月有效" value="90" key="90"></el-option> <el-option label="永久有效" value="0" key="0"></el-option> </el-select> </div> <div class="cart_column column_4">¥{{cart.price}}</div> <div class="cart_column column_4">删除</div> </div> </template> <script> export default { name: "CartItem", data(){ return { checked:false, } }, props:['cart', ] // 002:子组件接受父组件传过来的值 } </script>
1.页面刷新导致的vuex数据重置
解决方法:页面刷新前将数据存到SessionStorage
// App.vue <script> export default { name: 'App', created() { window.addEventListener('beforeunload',()=>{ sessionStorage.setItem('cart_length',this.$store.state.cart_length); }) } } </script>
2.不同页面显示的购物车小红圆圈数量不一致
在组件加载的时候,会执行vheader中的created方法,拿到sessionStorage的值
let cart_length = sessionStorage.getItem('cart_length'); this.$store.commit('add_cart',cart_length);
其中有一点:只有页面刷新的时候,才会拿到sessionStorage的值存放到vuex中,
如果页面不刷新,用户点击添加购物车(此时vuex存的购物车长度因为添加购物操作已经发生了变化),
我们组件再加载时,如果拿的是sessionStorage的值,其实拿的还是原来的那个值。
我们应该把拿值操作放到刷新页面之后
created(){ if (this.$store.state.cart_length === 0){ // 如果购物车没有数据 let cart_length = sessionStorage.getItem('cart_length'); // 就去sessionStorage中拿数据 this.$store.commit('add_cart',cart_length); // 并将数据存放到vuex中 } },