Luffy之购物车页面搭建
前面已经将一些课程加入购物车中,并保存到了后端的redis数据库中,此时做购物车页面时,我们需要将在前端向后端发送请求,用来获取数据数据
购物车页面
1.首先后端要将数据构建好,后端视图函数如下代码:
(post请求是将加入购物车的课程信息加入到redis中,其中对于价格在存储的时候要计算折扣后的价格,而get请求则是redis中取出数据到发送前端页面中)
cart/view:
from django.conf import settings
from rest_framework import status
from rest_framework.response import Response
from django_redis import get_redis_connection
from rest_framework.views import APIView
from courses.models import Course
from rest_framework.permissions import IsAuthenticated
from .utils import get_course_real_price
class CartAPIView(APIView): permission_classes = [IsAuthenticated] def post(self,request): """添加课程到购物车""" # 接受客户端提交过来的课程ID course_id = request.data.get("course_id") try: course = models.Course.objects.get(pk=course_id, status=0) except: return Response({"message": "当前课程不存在或者已经下架了"}, status=status.HTTP_400_BAD_REQUEST)
# 计算课程的真实价格
price = get_course_real_price(course)
# 把课程id和课程价格保存到购物车中redis中 # redis中使用hash类型保存数据 redis = get_redis_connection("cart") # 获取当前登陆用户的ID,并写入redis中 user_id = request.user.id pl = redis.pipeline() pl.multi() pl.hset("cart_%s" % user_id, course_id, str(course.price)) # 把当前课程默认为勾选状态[勾选状态也要保存到redis中] # redis中使用set类型保存数据 pl.sadd("cart_select_%s" % user_id, course_id) pl.execute() # 返回响应操作 return Response({"message": "success"}, status=status.HTTP_200_OK) def get(self,request): # 获取当前登陆用户 user_id = request.user.id # 从redis中获取所有的课程信息和勾选状态 redis = get_redis_connection("cart") course_list = redis.hgetall("cart_%s" % user_id) selected_list = redis.smembers("cart_select_%s" % user_id) # 构造数据返回给前端 data = [] for course_id, price in course_list.items(): course_id = course_id.decode() price = price.decode() try: course_info = models.Course.objects.get(pk=course_id) except: return Response({"message": "请求有误,请联系客服"}, status=status.HTTP_507_INSUFFICIENT_STORAGE) data.append({ "id": course_id, "price": price, "selected": course_id.encode() in selected_list, "course_img": HOST+course_info.course_img.url, "name": course_info.name, }) # 返回给客户端 return Response(data, status=status.HTTP_200_OK)
其中关于计算折扣的详细方法如下:
cart/utils:
1 from decimal import Decimal 2 3 def get_course_real_price(course): 4 price = course.price 5 st = course.price_service_type # 价格服务类型 6 if st is not None: 7 all_services = st.priceservices.all() # 当前价格类型的所有服务策略 8 if st != None and len(all_services) > 0: 9 if all_services[0].condition > 0: # 是否有设置了价格服务,没有设置价格服务的课程,服务为值None 10 # 1. 优惠条件值大于0,则属于满减 11 service_list = all_services 12 # 进行满减价格计算 13 real_sale = 0 # 保存满足条件的优惠值 14 for item in service_list: 15 item.condition = int(item.condition) 16 item.sale = int(item.sale) 17 if course.price > item.condition and real_sale <= item.sale: 18 real_sale = item.sale 19 price = course.price - real_sale 20 21 else: # 优惠条件值为0,则表示是限时折扣或者限时免费 22 if all_services[0].sale == "-1": 23 # 2. 限时免费 24 price = 0 25 else: 26 # 3. 限时折扣 27 # 把优惠值sale中的*去掉 28 sale = all_services[0].sale[1:] 29 price = course.price * Decimal(sale) 30 else: 31 # 原价 32 price = course.price 33 34 return "%.2f" % price
2.关于设置勾选的商品发送到后端保存以及按钮删除购物车课程
后端代码:
cart/view:(由于此时前端发送过来的数据含有数字,另外开一个类(CartCourseAPIView)处理此次请求)
post:前端携带相应的取消或添加勾选购物车内课程的选项,后端根据携带值得真假,做相应的增加或删除勾选项
delete:用于处理前端按钮删除某个购物车课程的处理,需要在购物车课程列表中删除对应键值对,并在勾选集合中删掉对应的id值
1 class CartCourseAPIView(APIView): 2 permission_classes = [IsAuthenticated] 3 4 def post(self,request,pk): 5 user = request.user 6 print("user_id",user.id) 7 try: 8 course = models.Course.objects.get(pk=pk) 9 except models.Course.DoesNotExist: 10 return Response({"message": "0"}, status=status.HTTP_400_BAD_REQUEST) 11 12 #获取勾选状态 13 is_selected = request.data.get("is_select") 14 15 #引入redis 16 redis = get_redis_connection("cart") 17 if is_selected: 18 # redis中增加当前课程id到勾选集合中 19 redis.sadd("cart_select_%s" % user.id, pk) 20 else: 21 # redis中删除当前课程id 22 redis.srem("cart_select_%s" % user.id, pk) 23 24 return Response({"message": "1"}, status=status.HTTP_200_OK) 25 26 def delete(self,request,pk): 27 28 user = request.user 29 try: 30 course = models.Course.objects.get(pk=pk) 31 except models.Course.DoesNotExist: 32 return Response({"message": "无效的课程标号"}, status=status.HTTP_400_BAD_REQUEST) 33 34 # 从购物车和勾选集合中删除指定的数据 35 redis = get_redis_connection("cart") 36 pl = redis.pipeline() 37 pl.multi() 38 pl.hdel("cart_%s" % user.id, pk) 39 pl.srem("cart_select_%s" % user.id, pk) 40 pl.execute() 41 42 return Response({"message": "删除成功!"}, status=status.HTTP_200_OK)
前端页面要做的一些功能:
加载数据时,从后端拿数据,发送请求:
cart.vue
1 //计算各种折算后,购物车的总价 2 total_price(){ 3 // 计算总价格 4 let cl = this.course_list; 5 let total = 0; 6 for(let i = 0;i<cl.length;i++){ 7 if(cl[i].selected){ 8 total+=parseFloat(cl[i].price); 9 } 10 } 11 total = total.toFixed(2); 12 this.total = total; 13 }, 14 }, 15 created() { 16 let _this = this; 17 // 发起请求获取购物车中的商品信息 18 _this.$axios.get("http://127.0.0.1:8000/cart/",{ 19 headers: { 20 'Authorization': 'JWT ' + _this.token 21 }, 22 responseType: 'json', 23 }).then(response=>{ 24 console.log("response.data",response.data) 25 _this.course_list = response.data; 26 this.total_price() 27 }) 28 29 },
勾选购物车内课程选项时:
1.在每次用户点击选项框时,向后台发送请求,更新后端redis中的勾选项集合(采用监视的方法,只要选项的值变动,便发送请求),
2.在发送请求成功后,需要通知父组件更新,结算的总结各
Template: <el-col :span="2" class="checkbox"><el-checkbox label="" v-model="course.selected" name="type"></el-checkbox></el-col> script标签内: watch:{ "course.selected":function(){ let _this = this; _this.$axios.post(`http://127.0.0.1:8000/cart/${this.course.id}`,{ is_select: _this.course.selected },{ headers:{ // 附带已经登录用户的jwt token 提供给后端,一定不能疏忽这个空格 'Authorization':'JWT '+_this.token }, responseType:"json", }).then(response=>{
//通知父组件更改价格 _this.$emit("change_select"); }).catch(error=>{ console.log( error.response ); }) } },
2.按钮删除购物车课程,需要做的有:
1.用delete请求向后端发送携带要删除课程id的值
2.在点击该删除按钮时,同时告知父组件应该删除该项课程(涉及到子传父的数据交互问题)
3.在点击该删除按钮时,应该刷新所勾选的购物车的课程结算金额,因为实在父组件中展示的总价,也要对父组件发送更新总价的通知
cartitem.vue中(cart的子组件)
1 template内: 2 <el-col :span="4" class="course-delete"><span @click="delete_course(course.id)">删除</span></el-col> 3 4 script内: 5 6 props:["course","course_key"], //父组件传递过来的数据 7 methods:{ 8 //按删除键删除购物车的课程 9 delete_course(course_id){ 10 let _this = this; 11 this.$axios.delete(`http://127.0.0.1:8000/cart/${this.course.id}`, { 12 headers: { 13 // 附带已经登录用户的jwt token 提供给后端,一定不能疏忽这个空格 14 'Authorization': 'JWT ' + _this.token 15 }, 16 responseType: "json", 17 }).then(response => { 18 19 // 发送信息给父组件,通过父组件删除当前子组件 20 _this.$emit("delete_course",_this.course_key); 21 }).catch(error => { 22 console.log(error.response); 23 }); 24 }, 25 },
cart.vue中(cartitem的父组件):
1 template: 2 <CartItem v-for="item,course_key in course_list" :course="item" @change_select="total_price" @delete_course="del_course" :course_key="course_key" /> 3 4 script:
5 export default { 6 name:"Cart", 7 data(){ 8 return { 9 total:0, 10 course_list:[], 11 token: localStorage.token || sessionStorage.token, 12 } 13 }, 14 15 components:{ 16 Header, 17 Footer, 18 CartItem, 19 }, 20 methods:{ 21 del_course(course_key) { 22 //course_key是通过字传父传回来,用于删除已删除的的购物车的课程 23 this.course_list.splice(course_key, 1); 24 // 重新计算总价格 25 this.total_price(); 26 }, 27 //计算各种折算后,购物车的总价 28 total_price(){ 29 // 计算总价格 30 let cl = this.course_list; 31 let total = 0; 32 for(let i = 0;i<cl.length;i++){ 33 if(cl[i].selected){ 34 total+=parseFloat(cl[i].price); 35 } 36 } 37 total = total.toFixed(2); 38 this.total = total; 39 }, 40 }, 41 created() { 42 let _this = this; 43 // 发起请求获取购物车中的商品信息 44 _this.$axios.get("http://127.0.0.1:8000/cart/",{ 45 headers: { 46 'Authorization': 'JWT ' + _this.token 47 }, 48 responseType: 'json', 49 }).then(response=>{ 50 console.log("response.data",response.data) 51 _this.course_list = response.data; 52 this.total_price() //在加载数据的时候也要对总价做出计算 53 }) 54 55 }, 56 }
详细的完整代码如下:
后端试图cart/view
from django.shortcuts import render # Create your views here. from django_redis import get_redis_connection from rest_framework import status from rest_framework.permissions import IsAuthenticated from rest_framework.response import Response from rest_framework.views import APIView from luffy.apps.cart.utils import get_course_real_price from luffy.apps.courses import models from luffy.settings import HOST class CartAPIView(APIView): permission_classes = [IsAuthenticated] def post(self,request): """添加课程到购物车""" # 接受客户端提交过来的课程ID course_id = request.data.get("course_id") try: course = models.Course.objects.get(pk=course_id, status=0) except: return Response({"message": "当前课程不存在或者已经下架了"}, status=status.HTTP_400_BAD_REQUEST) # 计算课程的真实价格,调用写好的的在utils的计算折扣的方法 price = get_course_real_price(course) # 把课程id和课程价格保存到购物车中redis中 # redis中使用hash类型保存数据 redis = get_redis_connection("cart") # 获取当前登陆用户的ID,并写入redis中 user_id = request.user.id pl = redis.pipeline() pl.multi() pl.hset("cart_%s" % user_id, course_id, price) # 把当前课程默认为勾选状态[勾选状态也要保存到redis中] # redis中使用set类型保存数据 pl.sadd("cart_select_%s" % user_id, course_id) pl.execute() # 返回响应操作 return Response({"message": "success"}, status=status.HTTP_200_OK) def get(self,request): # 获取当前登陆用户 user_id = request.user.id # 从redis中获取所有的课程信息和勾选状态 redis = get_redis_connection("cart") course_list = redis.hgetall("cart_%s" % user_id) selected_list = redis.smembers("cart_select_%s" % user_id) # 构造数据返回给前端 data = [] for course_id, price in course_list.items(): course_id = course_id.decode() price = price.decode() try: course_info = models.Course.objects.get(pk=course_id) except: return Response({"message": "请求有误,请联系客服"}, status=status.HTTP_507_INSUFFICIENT_STORAGE) data.append({ "id": course_id, "price": price, "selected": course_id.encode() in selected_list, "course_img": HOST+course_info.course_img.url, "name": course_info.name, }) # 返回给客户端 return Response(data, status=status.HTTP_200_OK) class CartCourseAPIView(APIView): permission_classes = [IsAuthenticated] def post(self,request,pk): user = request.user print("user_id",user.id) try: course = models.Course.objects.get(pk=pk) except models.Course.DoesNotExist: return Response({"message": "0"}, status=status.HTTP_400_BAD_REQUEST) #获取勾选状态 is_selected = request.data.get("is_select") #引入redis redis = get_redis_connection("cart") if is_selected: # redis中增加当前课程id到勾选集合中 redis.sadd("cart_select_%s" % user.id, pk) else: # redis中删除当前课程id redis.srem("cart_select_%s" % user.id, pk) return Response({"message": "1"}, status=status.HTTP_200_OK) def delete(self,request,pk): user = request.user try: course = models.Course.objects.get(pk=pk) except models.Course.DoesNotExist: return Response({"message": "无效的课程标号"}, status=status.HTTP_400_BAD_REQUEST) # 从购物车和勾选集合中删除指定的数据 redis = get_redis_connection("cart") pl = redis.pipeline() pl.multi() pl.hdel("cart_%s" % user.id, pk) pl.srem("cart_select_%s" % user.id, pk) pl.execute() return Response({"message": "删除成功!"}, status=status.HTTP_200_OK)
前端cart.vue(父组件):
1 <template> 2 <div class="cart"> 3 <Header/> 4 <div class="cart-info"> 5 <h3 class="cart-top">我的购物车 <span>共1门课程</span></h3> 6 <div class="cart-title"> 7 <el-row> 8 <el-col :span="2"> </el-col> 9 <el-col :span="10">课程</el-col> 10 <el-col :span="4">有效期</el-col> 11 <el-col :span="4">单价</el-col> 12 <el-col :span="4">操作</el-col> 13 </el-row> 14 </div> 15 <CartItem v-for="item in course_list" :course="item" @change_select="total_price" @delete_course="del_course" :course_key="course_key" /> 16 <div class="calc"> 17 <el-row> 18 <el-col :span="2"> </el-col> 19 <el-col :span="3"> 20 <el-checkbox label="全选" name="type"></el-checkbox></el-col> 21 <el-col :span="2" class="del"><i class="el-icon-delete"></i>删除</el-col> 22 <el-col :span="12" class="count">总计:¥{{total}}</el-col> 23 <el-col :span="3" class="cart-calc">去结算</el-col> 24 </el-row> 25 </div> 26 </div> 27 <Footer/> 28 </div> 29 </template> 30 31 <script> 32 import Header from "./common/Header" 33 import Footer from "./common/Footer" 34 import CartItem from "./common/CartItem" 35 export default { 36 name:"Cart", 37 data(){ 38 return { 39 total:0, 40 course_list:[], 41 token: localStorage.token || sessionStorage.token, 42 } 43 }, 44 45 components:{ 46 Header, 47 Footer, 48 CartItem, 49 }, 50 methods:{ 51 del_course(course_key) { 52 //course_key是通过字传父传回来,用于删除已删除的的购物车的课程 53 this.course_list.splice(course_key, 1); 54 // 重新计算总价格 55 this.total_price(); 56 }, 57 //计算各种折算后,购物车的总价 58 total_price(){ 59 // 计算总价格 60 let cl = this.course_list; 61 let total = 0; 62 for(let i = 0;i<cl.length;i++){ 63 if(cl[i].selected){ 64 total+=parseFloat(cl[i].price); 65 } 66 } 67 total = total.toFixed(2); 68 this.total = total; 69 }, 70 }, 71 created() { 72 let _this = this; 73 // 发起请求获取购物车中的商品信息 74 _this.$axios.get("http://127.0.0.1:8000/cart/",{ 75 headers: { 76 'Authorization': 'JWT ' + _this.token 77 }, 78 responseType: 'json', 79 }).then(response=>{ 80 console.log("response.data",response.data) 81 _this.course_list = response.data; 82 this.total_price() 83 }) 84 85 }, 86 } 87 </script> 88 89 <style scoped> 90 .cart{ 91 margin-top: 80px; 92 } 93 .cart-info{ 94 overflow: hidden; 95 width: 1200px; 96 margin: auto; 97 } 98 .cart-top{ 99 font-size: 18px; 100 color: #666; 101 margin: 25px 0; 102 font-weight: normal; 103 } 104 .cart-top span{ 105 font-size: 12px; 106 color: #d0d0d0; 107 display: inline-block; 108 } 109 .cart-title{ 110 background: #F7F7F7; 111 } 112 .cart-title .el-row,.cart-title .el-col{ 113 height: 80px; 114 font-size: 14px; 115 color: #333; 116 line-height: 80px; 117 } 118 .calc .el-col{ 119 height: 80px; 120 line-height: 80px; 121 } 122 .calc .el-row span{ 123 font-size: 18px!important; 124 } 125 .calc .el-row{ 126 font-size: 18px; 127 color: #666; 128 margin-bottom: 300px; 129 margin-top: 50px; 130 background: #F7F7F7; 131 } 132 .calc .del{ 133 134 } 135 .calc .el-icon-delete{ 136 margin-right: 15px; 137 font-size: 20px; 138 } 139 .calc .count{ 140 text-align: right; 141 margin-right:62px; 142 } 143 .calc .cart-calc{ 144 width: 159px; 145 height: 80px; 146 border: none; 147 background: #ffc210; 148 font-size: 18px; 149 color: #fff; 150 text-align: center; 151 cursor: pointer; 152 } 153 </style>
前端cartitem.vue(子组件):
1 <template> 2 <div class="cart-item"> 3 <el-row> 4 <el-col :span="2" class="checkbox"><el-checkbox label="" v-model="course.selected" name="type"></el-checkbox></el-col> 5 <el-col :span="10" class="course-info"> 6 <img :src="course.course_img" alt=""> 7 <span>{{course.name}}</span> 8 </el-col> 9 <el-col :span="4"> 10 <el-select v-model="duration"> 11 <el-option v-for="item in options" :key="item.value" :label="item.label" :value="item.value"></el-option> 12 </el-select> 13 </el-col> 14 <el-col :span="4" class="course-price">¥{{course.price}}</el-col> 15 <el-col :span="4" class="course-delete"><span @click="delete_course(course.id)">删除</span></el-col> 16 </el-row> 17 </div> 18 </template> 19 20 <script> 21 export default { 22 name:"CartItem", 23 24 props:["course","course_key"], 25 26 data(){ 27 return { 28 token: localStorage.token || sessionStorage.token, 29 duration: 60, 30 options:[ 31 {value:30,label:"一个月有效"}, 32 {value:60,label:"二个月有效"}, 33 {value:90,label:"三个月有效"}, 34 {value:-1,label:"永久有效"}, 35 ], 36 37 } 38 }, 39 mounted(){ 40 41 }, 42 43 methods:{ 44 //按删除键删除购物车的课程 45 delete_course(course_id){ 46 let _this = this; 47 this.$axios.delete(`http://127.0.0.1:8000/cart/${this.course.id}`, { 48 headers: { 49 // 附带已经登录用户的jwt token 提供给后端,一定不能疏忽这个空格 50 'Authorization': 'JWT ' + _this.token 51 }, 52 responseType: "json", 53 }).then(response => { 54 55 // 发送信息给父组件,通过父组件删除当前子组件 56 _this.$emit("delete_course",_this.course_key); 57 }).catch(error => { 58 console.log(error.response); 59 }); 60 }, 61 }, 62 watch:{ 63 "course.selected":function(){ 64 let _this = this; 65 _this.$axios.post(`http://127.0.0.1:8000/cart/${this.course.id}`,{ 66 is_select: _this.course.selected 67 },{ 68 headers:{ 69 // 附带已经登录用户的jwt token 提供给后端,一定不能疏忽这个空格 70 'Authorization':'JWT '+_this.token 71 }, 72 responseType:"json", 73 }).then(response=>{ 74 _this.$emit("change_select"); 75 }).catch(error=>{ 76 console.log( error.response ); 77 }) 78 } 79 }, 80 } 81 </script> 82 83 <style scoped> 84 .cart-item{ 85 height: 250px; 86 } 87 .cart-item .el-row{ 88 height: 100%; 89 } 90 .course-delete{ 91 font-size: 14px; 92 color: #ffc210; 93 cursor: pointer; 94 } 95 .el-checkbox,.el-select,.course-price,.course-delete{ 96 display: flex; 97 align-items: center; 98 justify-content: center; 99 height: 100%; 100 } 101 .el-checkbox{ 102 padding-top: 55px; 103 } 104 .el-select{ 105 padding-top: 45px; 106 width: 118px; 107 height: 28px; 108 font-size: 12px; 109 color: #666; 110 line-height: 18px; 111 } 112 .course-info img{ 113 width: 175px; 114 height: 115px; 115 margin-right: 35px; 116 vertical-align: middle; 117 } 118 .cart-item .el-col{ 119 padding: 67px 10px; 120 vertical-align: middle!important; 121 } 122 .course-info{ 123 124 } 125 </style>