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)
View Code

前端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">&nbsp;</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">&nbsp;</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    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      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>
View Code

前端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      118px;
107     height: 28px;
108     font-size: 12px;
109     color: #666;
110     line-height: 18px;
111 }
112 .course-info img{
113      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>
View Code
原文地址:https://www.cnblogs.com/Mixtea/p/10639277.html