• vue --06 购物车的实现


    购物车实现

    创建子应用 cart

    cd luffy/apps
    python ../../manage.py startapp cart

    注册子应用cart

    INSTALLED_APPS = [
        'ckeditor',  # 富文本编辑器
        'ckeditor_uploader',  # 富文本编辑器上传图片模块
    
        'home',
        'users',
        'courses',
        'cart',
    ]

    因为购物车中的商品(课程)信息会经常被用户操作,所以为了减轻服务器的压力,可以选择把购物车信息通过redis来存储.

    配置信息 (settings)

    # 设置redis缓存
    CACHES = {
        # 默认缓存
        ....
        
        "cart":{
            "BACKEND": "django_redis.cache.RedisCache",
            "LOCATION": "redis://127.0.0.1:6379/3",
            "OPTIONS": {
                "CLIENT_CLASS": "django_redis.client.DefaultClient",
            }
        },
    }

    接下来商品信息存储以下内容:

    course_id      购物车中的商品ID
    course_expire  购物车中商品的有效期
    is_selected    购物车中对应商品的勾选状态
    
    # 上面三个数据,实际上存储到redis中,要以什么类型来存储呢?
    # redis一共5种数据类型,我们就应该考虑到哪种数据类型保存上面的数据最方便我们读写.
    cart_<user_id>: {
      "商品ID1":"有效期",
      "商品ID1":"有效期",
      "商品ID1":"有效期",
    }
        
    # 把已经勾选的商品ID记录到无序集合中
    cart_selected_<user_id>:{
        "商品ID1",
        "商品ID2",
        "商品ID3",
    }

    添加课程商品到购物车的API接口实现

    cart/views.py视图,代码:

    from django.shortcuts import render
    
    # Create your views here.
    from rest_framework.views import APIView
    from courses.models import Course
    from rest_framework.response import Response
    from django_redis import get_redis_connection
    from rest_framework.permissions import IsAuthenticated
    from rest_framework import status
    class CartAPIView(APIView):
        permission_classes = [IsAuthenticated]
        """购物车视图"""
        def post(self,request):
            """购物车添加商品"""
            # 获取客户端发送过来的课程ID
            course_id = request.data.get("course_id")
            # 验证课程ID是否有效
            try:
                Course.objects.get(pk=course_id,is_delete=False,is_show=True)
            except Course.DoesNotExist:
                return Response({"message":"当前课程不存在!"},status=status.HTTP_400_BAD_REQUEST)
    
            # 组装基本数据[课程ID,有效期]保存到redis
            redis = get_redis_connection("cart")
            # user_id = 1
            user_id = request.user.id
            try:
                # 添加一个成员到指定名称的hash数据中[如果对应名称的hash数据不存在,则自动创建]
                # hset(名称,键,值)
                redis.hset("cart_%s" % user_id, course_id, -1) # -1表示购买的课程永久有效
    
                # 添加一个成员到制定名称的set数据中[如果对应名称的set数据不存在,则自动创建]
                # sadd(名称,成员)
                redis.sadd("cart_selected_%s" % user_id, course_id )
    
            except:
                return Response({"message": "添加课程到购物车失败!请联系客服人员~"},status=status.HTTP_507_INSUFFICIENT_STORAGE)
    
            # 返回结果
            return Response({"message":"成功添加课程到购物车!"},status=status.HTTP_200_OK)
    View Code

    提供访问路由

    总路由,代码

    urlpatterns = [
        ...
        path('cart/', include("cart.urls")),
    ]

    子应用路由cart/urls.py,代码:

    from django.urls import path, re_path
    from . import views
    urlpatterns = [
        path(r"course/",views.CartAPIView.as_view()),
    ]

    默认用户添加课程到购物车就已经勾选了商品,视图代码:

    from django.shortcuts import render
    
    # Create your views here.
    from rest_framework.views import APIView
    from courses.models import Course
    from rest_framework.response import Response
    from django_redis import get_redis_connection
    from rest_framework.permissions import IsAuthenticated
    from rest_framework import status
    class CartAPIView(APIView):
        permission_classes = [IsAuthenticated]
        """购物车视图"""
        def post(self,request):
            """购物车添加商品"""
            # 获取客户端发送过来的课程ID
            course_id = request.data.get("course_id")
            # 验证课程ID是否有效
            try:
                Course.objects.get(pk=course_id,is_delete=False,is_show=True)
            except Course.DoesNotExist:
                return Response({"message":"当前课程不存在!"},status=status.HTTP_400_BAD_REQUEST)
    
            # 组装基本数据[课程ID,有效期]保存到redis
            redis = get_redis_connection("cart")
            # user_id = 1
            user_id = request.user.id
            # transation: 事务
            # 作用: 可以设置多个数据库操作看成一个整体,这个整理里面每一条数据库操作都成功了,事务才算成功,
            #      如果出现其中任意一个数据库操作失败,则整体一起失败!
            # 事务可以提供 提交事务 和 回滚事务 的功能
            # 不仅mysql中存在事务,在redis中也有事务的概念,但是叫"管道 pipeline"
            try:
                # 创建事务[管道]对象
                pipeline = redis.pipeline()
                # 开启事务
                pipeline.multi()
                # 添加一个成员到指定名称的hash数据中[如果对应名称的hash数据不存在,则自动创建]
                # hset(名称,键,值)
                pipeline.hset("cart_%s" % user_id, course_id, -1) # -1表示购买的课程永久有效
    
                # 添加一个成员到制定名称的set数据中[如果对应名称的set数据不存在,则自动创建]
                # sadd(名称,成员)
                pipeline.sadd("cart_selected_%s" % user_id, course_id )
    
                # 提交事务[如果不提交,则事务会自动回滚]
                pipeline.execute()
    
            except:
                return Response({"message": "添加课程到购物车失败!请联系客服人员~"},status=status.HTTP_507_INSUFFICIENT_STORAGE)
    
            # 返回结果
            return Response({"message": "成功添加课程到购物车!"}, status=status.HTTP_200_OK)
    View Code

    前端提交课程到后端添加购物车数据

    Detai.vue

    <template>
        <div class="detail">
          <Header/>
          <div class="main">
            <div class="course-info">
              <div class="wrap-left">
                <video-player class="video-player vjs-custom-skin"
                   ref="videoPlayer"
                   :playsinline="true"
                   :options="playerOptions"
                   @play="onPlayerPlay($event)"
                   @pause="onPlayerPause($event)"
                >
                </video-player>
              </div>
              <div class="wrap-right">
                <h3 class="course-name">{{course.name}}</h3>
                <p class="data">{{course.students}}人在学&nbsp;&nbsp;&nbsp;&nbsp;课程总时长:{{course.lessons}}课时/{{course.lessons==course.pub_lessons?'更新完成':('已更新'+course.pub_lessons+"课程")}}&nbsp;&nbsp;&nbsp;&nbsp;难度:{{course.course_level}}</p>
                <div class="sale-time">
                  <p class="sale-type">限时免费</p>
                  <p class="expire">距离结束:仅剩 01天 04小时 33分 <span class="second">08</span> 秒</p>
                </div>
                <p class="course-price">
                  <span>活动价</span>
                  <span class="discount">¥0.00</span>
                  <span class="original">¥{{course.price}}</span>
                </p>
                <div class="buy">
                  <div class="buy-btn">
                    <button class="buy-now">立即购买</button>
                    <button class="free">免费试学</button>
                  </div>
                  <div @click="cartAddHander" class="add-cart"><img src="@/assets/cart-yellow.svg" alt="">加入购物车</div>
                </div>
              </div>
            </div>
            <div class="course-tab">
              <ul class="tab-list">
                <li :class="tabIndex==1?'active':''" @click="tabIndex=1">详情介绍</li>
                <li :class="tabIndex==2?'active':''" @click="tabIndex=2">课程章节 <span :class="tabIndex!=2?'free':''">(试学)</span></li>
                <li :class="tabIndex==3?'active':''" @click="tabIndex=3">用户评论 (42)</li>
                <li :class="tabIndex==4?'active':''" @click="tabIndex=4">常见问题</li>
              </ul>
            </div>
            <div class="course-content">
              <div class="course-tab-list">
                <div class="tab-item" v-if="tabIndex==1">
                  <div v-html="course.brief"></div>
                </div>
                <div class="tab-item" v-if="tabIndex==2">
                  <div class="tab-item-title">
                    <p class="chapter">课程章节</p>
                    <p class="chapter-length">共{{chapter_list.length}}章 147个课时</p>
                  </div>
                  <div class="chapter-item" v-for="chapter in chapter_list">
                    <p class="chapter-title"><img src="@/assets/1.svg" alt="">第{{chapter.chapter}}章·{{chapter.name}}</p>
                    <ul class="lesson-list">
                      <li class="lesson-item" v-for="lesson in chapter.coursesections">
                        <p class="name"><span class="index">{{chapter.chapter}}-{{lesson.id}}</span> {{lesson.name}}<span class="free" v-if="lesson.free_trail">免费</span></p>
                        <p class="time">{{lesson.duration}} <img src="@/assets/chapter-player.svg"></p>
                        <button class="try" v-if="lesson.free_trail"><router-link :to="{path: '/player',query:{'vid':lesson.section_link}}">立即试学</router-link></button>
                        <button class="try" v-else>立即购买</button>
                      </li>
    
                    </ul>
                  </div>
                </div>
                <div class="tab-item" v-if="tabIndex==3">
                  用户评论
                </div>
                <div class="tab-item" v-if="tabIndex==4">
                  常见问题
                </div>
              </div>
              <div class="course-side">
                 <div class="teacher-info">
                   <h4 class="side-title"><span>授课老师</span></h4>
                   <div class="teacher-content">
                     <div class="cont1">
                       <img :src="course.teacher.image">
                       <div class="name">
                         <p class="teacher-name">{{course.teacher.name}} {{course.teacher.title}}</p>
                         <p class="teacher-title">{{course.teacher.signature}}</p>
                       </div>
                     </div>
                     <p class="narrative" >Linux运维技术专家,老男孩Linux金牌讲师,讲课风趣幽默、深入浅出、声音洪亮到爆炸</p>
                   </div>
                 </div>
              </div>
            </div>
          </div>
          <Footer/>
        </div>
    </template>
    
    <script>
    import Header from "./common/Header"
    import Footer from "./common/Footer"
    
    import {videoPlayer} from 'vue-video-player';
    
    export default {
        name: "Detail",
        data(){
          return {
            token:sessionStorage.token || localStorage.token,
            user_id:sessionStorage.user_id || localStorage.user_id,
            user_name:sessionStorage.user_name || localStorage.user_name,
            tabIndex:1,  // 当前选项卡显示的下标
            course_id:0, // 当前页面对应的课程ID
            course: {
                teacher: {},
            },  // 课程详情信息
            chapter_list:{},
            playerOptions: {
              playbackRates: [0.7, 1.0, 1.5, 2.0], // 播放速度
              autoplay: false, //如果true,则自动播放
              muted: false, // 默认情况下将会消除任何音频。
              loop: false, // 循环播放
              preload: 'auto',  // 建议浏览器在<video>加载元素后是否应该开始下载视频数据。auto浏览器选择最佳行为,立即开始加载视频(如果浏览器支持)
              language: 'zh-CN',
              aspectRatio: '16:9', // 将播放器置于流畅模式,并在计算播放器的动态大小时使用该值。值应该代表一个比例 - 用冒号分隔的两个数字(例如"16:9""4:3")
              fluid: true, // 当true时,Video.js player将拥有流体大小。换句话说,它将按比例缩放以适应其容器。
              sources: [{ // 播放资源和资源格式
                type: "video/mp4",
                src: "http://img.ksbbs.com/asset/Mon_1703/05cacb4e02f9d9e.mp4" //你的视频地址(必填)
              }],
              poster: "../static/courses/675076.jpeg", //视频封面图
               document.documentElement.clientWidth, // 默认视频全屏时的最大宽度
              notSupportedMessage: '此视频暂无法播放,请稍后再试', //允许覆盖Video.js无法播放媒体源时显示的默认信息。
            }
          }
        },
        watch:{
          course(data){
            // 替换视频地址
            this.playerOptions.sources[0].src = data.video;
            // 替换视频封面
            this.playerOptions.poster = data.course_img;
            // 替换科恒信息中的详情介绍里面的图片路径
            while(data.brief.search(`"/media`) != -1 ){
              data.brief = data.brief.replace(`"/media`,`"${this.$settings.Host}/media`)
            }
          },
          tabIndex(data){
            if(data==2){
              //获取当前课程对应的章节列表和课时列表
              this.$axios.get(`${this.$settings.Host}/courses/chapters/?course=${this.course_id}`).then(response=>{
                this.chapter_list = response.data;
              }).catch(error=>{
                console.log(error.response)
              })
            }
          }
        },
        created(){
          // 获取当前课程ID
          this.course_id = this.$route.query.id - 0;
          // 判断ID基本有效性
          let _this = this;
          if( isNaN(this.course_id) || this.course_id < 1 ){
            _this.$alert("无效的课程ID!","错误",{
              callback(){
                _this.$router.go(-1);
              }});
          }
          // 发送请求获取后端课程数据
          this.$axios.get(this.$settings.Host+`/courses/detail/${this.course_id}/`).then(response=>{
            this.course = response.data;
            // 修改视频中的封面图片
            this.playerOptions.poster = this.course.course_img;
          }).catch(error=>{
            console.log(error.response)
          });
    
    
        },
        methods: {
          // 视频播放事件
          onPlayerPlay(player) {
            alert("play");
          },
          // 视频暂停播放事件
          onPlayerPause(player){
            alert("pause");
          },
          // 视频插件初始化
          player() {
            return this.$refs.videoPlayer.player;
          },
          // 添加商品课程到购物车
          cartAddHander(){
            // 1. 判断用户是否已经登录了.
            if(!this.token){
              this.$confirm("对不起,您尚未登录!请登录",'提示').then(() => {
                this.$router.push("/login");
              });
            }
    
            // 2. 发起请求
            this.$axios.post(this.$settings.Host+`/carts/course/`,{
              course_id: this.course_id,
            },{
              headers:{
                // 注意:jwt后面必须有且只有一个空格!!!!
                "Authorization":"jwt " + this.token
              }
            }).then(response=>{
    
              // 获取购物城中商品总数
    
              // 添加购物车成功!
              this.$message(response.data.message,"提示!",{
                duration: 2000, // 单位: 毫秒
              });
    
            }).catch(error=>{
    
              console.log(error.response);
            })
          }
        },
        components:{
          Header,
          Footer,
          videoPlayer,
        }
    }
    </script>
    
    <style scoped>
    .main{
      background: #fff;
      padding-top: 30px;
    }
    .course-info{
       1200px;
      margin: 0 auto;
      overflow: hidden;
    }
    .wrap-left{
      float: left;
       690px;
      height: 388px;
      background-color: #000;
    }
    .wrap-right{
      float: left;
      position: relative;
      height: 388px;
    }
    .course-name{
      font-size: 20px;
      color: #333;
      padding: 10px 23px;
      letter-spacing: .45px;
    }
    .data{
      padding-left: 23px;
      padding-right: 23px;
      padding-bottom: 16px;
      font-size: 14px;
      color: #9b9b9b;
    }
    .sale-time{
       464px;
      background: #fa6240;
      font-size: 14px;
      color: #4a4a4a;
      padding: 10px 23px;
      overflow: hidden;
    }
    .sale-type {
      font-size: 16px;
      color: #fff;
      letter-spacing: .36px;
      float: left;
    }
    .sale-time .expire{
      font-size: 14px;
      color: #fff;
      float: right;
    }
    .sale-time .expire .second{
       24px;
      display: inline-block;
      background: #fafafa;
      color: #5e5e5e;
      padding: 6px 0;
      text-align: center;
    }
    .course-price{
      background: #fff;
      font-size: 14px;
      color: #4a4a4a;
      padding: 5px 23px;
    }
    .discount{
      font-size: 26px;
      color: #fa6240;
      margin-left: 10px;
      display: inline-block;
      margin-bottom: -5px;
    }
    .original{
      font-size: 14px;
      color: #9b9b9b;
      margin-left: 10px;
      text-decoration: line-through;
    }
    .buy{
       464px;
      padding: 0px 23px;
      position: absolute;
      left: 0;
      bottom: 20px;
      overflow: hidden;
    }
    .buy .buy-btn{
      float: left;
    }
    .buy .buy-now{
       125px;
      height: 40px;
      border: 0;
      background: #ffc210;
      border-radius: 4px;
      color: #fff;
      cursor: pointer;
      margin-right: 15px;
      outline: none;
    }
    .buy .free{
       125px;
      height: 40px;
      border-radius: 4px;
      cursor: pointer;
      margin-right: 15px;
      background: #fff;
      color: #ffc210;
      border: 1px solid #ffc210;
    }
    .add-cart{
      float: right;
      font-size: 14px;
      color: #ffc210;
      text-align: center;
      cursor: pointer;
      margin-top: 10px;
    }
    .add-cart img{
       20px;
      height: 18px;
      margin-right: 7px;
      vertical-align: middle;
    }
    
    .course-tab{
         100%;
        background: #fff;
        margin-bottom: 30px;
        box-shadow: 0 2px 4px 0 #f0f0f0;
    
    }
    .course-tab .tab-list{
         1200px;
        margin: auto;
        color: #4a4a4a;
        overflow: hidden;
    }
    .tab-list li{
        float: left;
        margin-right: 15px;
        padding: 26px 20px 16px;
        font-size: 17px;
        cursor: pointer;
    }
    .tab-list .active{
        color: #ffc210;
        border-bottom: 2px solid #ffc210;
    }
    .tab-list .free{
        color: #fb7c55;
    }
    .course-content{
         1200px;
        margin: 0 auto;
        background: #FAFAFA;
        overflow: hidden;
        padding-bottom: 40px;
    }
    .course-tab-list{
         880px;
        height: auto;
        padding: 20px;
        background: #fff;
        float: left;
        box-sizing: border-box;
        overflow: hidden;
        position: relative;
        box-shadow: 0 2px 4px 0 #f0f0f0;
    }
    .tab-item{
         880px;
        background: #fff;
        padding-bottom: 20px;
        box-shadow: 0 2px 4px 0 #f0f0f0;
    }
    .tab-item-title{
        justify-content: space-between;
        padding: 25px 20px 11px;
        border-radius: 4px;
        margin-bottom: 20px;
        border-bottom: 1px solid #333;
        border-bottom-color: rgba(51,51,51,.05);
        overflow: hidden;
    }
    .chapter{
        font-size: 17px;
        color: #4a4a4a;
        float: left;
    }
    .chapter-length{
        float: right;
        font-size: 14px;
        color: #9b9b9b;
        letter-spacing: .19px;
    }
    .chapter-title{
        font-size: 16px;
        color: #4a4a4a;
        letter-spacing: .26px;
        padding: 12px;
        background: #eee;
        border-radius: 2px;
        display: -ms-flexbox;
        display: flex;
        -ms-flex-align: center;
        align-items: center;
    }
    .chapter-title img{
         18px;
        height: 18px;
        margin-right: 7px;
        vertical-align: middle;
    }
    .lesson-list{
        padding:0 20px;
    }
    .lesson-list .lesson-item{
        padding: 15px 20px 15px 36px;
        cursor: pointer;
        justify-content: space-between;
        position: relative;
        overflow: hidden;
    }
    .lesson-item .name{
        font-size: 14px;
        color: #666;
        float: left;
    }
    .lesson-item .index{
        margin-right: 5px;
    }
    .lesson-item .free{
        font-size: 12px;
        color: #fff;
        letter-spacing: .19px;
        background: #ffc210;
        border-radius: 100px;
        padding: 1px 9px;
        margin-left: 10px;
    }
    .lesson-item .time{
        font-size: 14px;
        color: #666;
        letter-spacing: .23px;
        opacity: 1;
        transition: all .15s ease-in-out;
        float: right;
    }
    .lesson-item .time img{
         18px;
        height: 18px;
        margin-left: 15px;
        vertical-align: text-bottom;
    }
    .lesson-item .try{
         86px;
        height: 28px;
        background: #ffc210;
        border-radius: 4px;
        font-size: 14px;
        color: #fff;
        position: absolute;
        right: 20px;
        top: 10px;
        opacity: 0;
        transition: all .2s ease-in-out;
        cursor: pointer;
        outline: none;
        border: none;
    }
    .lesson-item:hover{
        background: #fcf7ef;
        box-shadow: 0 0 0 0 #f3f3f3;
    }
    .lesson-item:hover .name{
        color: #333;
    }
    .lesson-item:hover .try{
        opacity: 1;
    }
    
    .course-side{
         300px;
        height: auto;
        margin-left: 20px;
        float: right;
    }
    .teacher-info{
        background: #fff;
        margin-bottom: 20px;
        box-shadow: 0 2px 4px 0 #f0f0f0;
    }
    .side-title{
        font-weight: normal;
        font-size: 17px;
        color: #4a4a4a;
        padding: 18px 14px;
        border-bottom: 1px solid #333;
        border-bottom-color: rgba(51,51,51,.05);
    }
    .side-title span{
        display: inline-block;
        border-left: 2px solid #ffc210;
        padding-left: 12px;
    }
    
    .teacher-content{
        padding: 30px 20px;
        box-sizing: border-box;
    }
    
    .teacher-content .cont1{
        margin-bottom: 12px;
        overflow: hidden;
    }
    
    .teacher-content .cont1 img{
         54px;
        height: 54px;
        margin-right: 12px;
        float: left;
    }
    .teacher-content .cont1 .name{
        float: right;
    }
    .teacher-content .cont1 .teacher-name{
         188px;
        font-size: 16px;
        color: #4a4a4a;
        padding-bottom: 4px;
    }
    .teacher-content .cont1 .teacher-title{
         188px;
        font-size: 13px;
        color: #9b9b9b;
        white-space: nowrap;
    }
    .teacher-content .narrative{
        font-size: 14px;
        color: #666;
        line-height: 24px;
    }
    </style>
    View Code

    后端增加返回一个购物车的商品课程总数

    from django.shortcuts import render
    
    # Create your views here.
    from rest_framework.views import APIView
    from courses.models import Course
    from rest_framework.response import Response
    from django_redis import get_redis_connection
    from rest_framework.permissions import IsAuthenticated
    from rest_framework import status
    class CartAPIView(APIView):
        permission_classes = [IsAuthenticated]
        """购物车视图"""
        def post(self,request):
            """购物车添加商品"""
            # 获取客户端发送过来的课程ID
            course_id = request.data.get("course_id")
            # 验证课程ID是否有效
            try:
                Course.objects.get(pk=course_id,is_delete=False,is_show=True)
            except Course.DoesNotExist:
                return Response({"message":"当前课程不存在!"},status=status.HTTP_400_BAD_REQUEST)
    
            # 组装基本数据[课程ID,有效期]保存到redis
            redis = get_redis_connection("cart")
            # user_id = 1
            user_id = request.user.id
            # transation: 事务
            # 作用: 可以设置多个数据库操作看成一个整体,这个整理里面每一条数据库操作都成功了,事务才算成功,
            #      如果出现其中任意一个数据库操作失败,则整体一起失败!
            # 事务可以提供 提交事务 和 回滚事务 的功能
            # 不仅mysql中存在事务,在redis中也有事务的概念,但是叫"管道 pipeline"
            try:
                # 创建事务[管道]对象
                pipeline = redis.pipeline()
                # 开启事务
                pipeline.multi()
                # 添加一个成员到指定名称的hash数据中[如果对应名称的hash数据不存在,则自动创建]
                # hset(名称,键,值)
                pipeline.hset("cart_%s" % user_id, course_id, -1) # -1表示购买的课程永久有效
    
                # 添加一个成员到制定名称的set数据中[如果对应名称的set数据不存在,则自动创建]
                # sadd(名称,成员)
                pipeline.sadd("cart_selected_%s" % user_id, course_id )
    
                # 提交事务[如果不提交,则事务会自动回滚]
                pipeline.execute()
    
            except:
                return Response({"message": "添加课程到购物车失败!请联系客服人员~"},status=status.HTTP_507_INSUFFICIENT_STORAGE)
    
            # 返回结果,返回购物车中的商品数量
            count = redis.hlen("cart_%s" % user_id)
    
            return Response({
                "message": "成功添加课程到购物车!",
                "count": count,
            }, status=status.HTTP_200_OK)
    View Code

    前端展示商品课程的总数

    获取商品总数是在头部组件中使用到,并展示出来,但是我们后面可以在购物车中,或者商品课程的详情页中修改购物车中商品总数,因为对于一些数据,需要在多个组件中共享,这种情况,我们可以使用本地存储来完成,但是也可以通过vuex组件来完成这个功能。

    安装vuex
    npm install -S vuex
    把vuex注册到vue中
    1. 在src目录下创建store目录,并在store目录下创建一个index.js文件,index.js文件代码:

      import Vue from 'vue'
      import Vuex from 'vuex'
      
      Vue.use(Vuex);
      
      export const store = new Vuex.Store({
        // 数据仓库,类似vue里面的data
        state: {
      
        },
        // 数据操作方法,类似vue里面的methods
        mutations: {
      
        }
      });
    2. 把上面index.js中创建的store对象注册到main.js的vue中。

      // The Vue build version to load with the `import` command
      // (runtime-only or standalone) has been set in webpack.base.conf with an alias.
      import Vue from 'vue'
      import App from './App'
      
      import router from './routers/index';
      import store from './store/index';
      
      // 手动的自定义全局配置
      import settings from "./settings"
      Vue.prototype.$settings = settings;
      
      // elementUI 导入
      import ElementUI from 'element-ui';
      import 'element-ui/lib/theme-chalk/index.css';
      // 调用插件
      Vue.use(ElementUI);
      
      import "../static/css/reset.css"
      
      import axios from 'axios'; // 从node_modules目录中导入包
      
      // 允许ajax发送请求时附带cookie
      axios.defaults.withCredentials = true;
      
      Vue.prototype.$axios = axios; // 把对象挂载vue中
      
      
      Vue.config.productionTip = false;
      
      // 导入gt极验
      import '../static/js/gt.js';
      
      // vue-video视频播放插件
      require('video.js/dist/video-js.css');
      require('vue-video-player/src/custom-theme.css');
      import VideoPlayer from 'vue-video-player'
      Vue.use(VideoPlayer);
      
      /* eslint-disable no-new */
      new Vue({
        el: '#app',
        router,
        store,
        components: { App },
        template: '<App/>'
      })
    3. 接下来,我们就可以在组件使用到store中state里面保存的共享数据了.

      先到vuex中添加数据,store/index.js,代码

      import Vue from 'vue'
      import Vuex from 'vuex'
      
      Vue.use(Vuex);
      
      export default new Vuex.Store({
        // 数据仓库,类似vue里面的data
        state: {
          // 购物车数据
          cart:{
            count: 0,
            // course_list: [], // 购物车里面的商品列表信息
          }
        },
        // 数据操作方法,类似vue里面的 methods
        mutations: {
          // data是调用方法,传递的购物车相关的参数
          addcart(state,data){
            // 修改商品课程的总数
            state.cart.count = data.count;
            // state.cart.course_list = data.course_list;
          }
        }
      });

    在Header.vue头部组件中,直接读取store里面的数据

    <b class="goods-number">{{this.$store.state.cart.count}}</b>
    // this是可以省略不写。
    <b class="goods-number">{{$store.state.cart.count}}</b>

    我们可以在Detail.vue课程详情的组件中, 修改商品总数。

    <template>
      <div class="detail">
        <Header/>
        <div class="main">
          <div class="course-info">
            <div class="wrap-left">
              <video-player class="video-player vjs-custom-skin"
                            ref="videoPlayer"
                            :playsinline="true"
                            :options="playerOptions"
                            @play="onPlayerPlay($event)"
                            @pause="onPlayerPause($event)"
              >
              </video-player>
            </div>
            <div class="wrap-right">
              <h3 class="course-name">{{course.name}}</h3>
              <p class="data">{{course.students}}人在学&nbsp;&nbsp;&nbsp;&nbsp;课程总时长:{{course.lessons}}课时/{{course.lessons==course.pub_lessons?'更新完成':('已更新'+course.pub_lessons+"课程")}}&nbsp;&nbsp;&nbsp;&nbsp;难度:{{course.course_level}}</p>
              <div class="sale-time">
                <p class="sale-type">限时免费</p>
                <p class="expire">距离结束:仅剩 01天 04小时 33分 <span class="second">08</span></p>
              </div>
              <p class="course-price">
                <span>活动价</span>
                <span class="discount">¥0.00</span>
                <span class="original">¥{{course.price}}</span>
              </p>
              <div class="buy">
                <div class="buy-btn">
                  <button class="buy-now">立即购买</button>
                  <button class="free">免费试学</button>
                </div>
                <div class="add-cart" @click="cartAddHander"><img src="@/assets/cart-yellow.svg" alt="">加入购物车</div>
              </div>
            </div>
          </div>
          <div class="course-tab">
            <ul class="tab-list">
              <li :class="tabIndex==1?'active':''" @click="tabIndex=1">详情介绍</li>
              <li :class="tabIndex==2?'active':''" @click="tabIndex=2">课程章节 <span :class="tabIndex!=2?'free':''">(试学)</span>
              </li>
              <li :class="tabIndex==3?'active':''" @click="tabIndex=3">用户评论 (42)</li>
              <li :class="tabIndex==4?'active':''" @click="tabIndex=4">常见问题</li>
            </ul>
          </div>
          <div class="course-content">
            <div class="course-tab-list">
              <div class="tab-item" v-if="tabIndex==1">
                <div v-html="course.brief"></div>
              </div>
              <div class="tab-item" v-if="tabIndex==2">
                <div class="tab-item-title">
                  <p class="chapter">课程章节</p>
                  <p class="chapter-length">共{{chapter_list.length}}章 147个课时</p>
                </div>
                <div class="chapter-item" v-for="chapter in chapter_list">
                  <p class="chapter-title"><img src="@/assets/1.svg" alt="">第{{chapter.chapter}}章·{{chapter.name}}</p>
                  <ul class="lesson-list">
                    <li class="lesson-item" v-for="lesson in chapter.coursesections">
                      <p class="name"><span class="index">{{chapter.chapter}}-{{lesson.id}}</span> {{lesson.name}}<span
                        class="free" v-if="lesson.free_trail">免费</span></p>
                      <p class="time">{{lesson.duration}} <img src="@/assets/chapter-player.svg"></p>
                      <button class="try" v-if="lesson.free_trail">
                        <router-link :to="{path:'/player',query:{'vid':lesson.section_link}}">立即试学</router-link>
                      </button>
                      <button class="try" v-else>立即购买</button>
                    </li>
    
                  </ul>
                </div>
              </div>
              <div class="tab-item" v-if="tabIndex==3">
                用户评论
              </div>
              <div class="tab-item" v-if="tabIndex==4">
                常见问题
              </div>
            </div>
            <div class="course-side">
              <div class="teacher-info">
                <h4 class="side-title"><span>授课老师</span></h4>
                <div class="teacher-content">
                  <div class="cont1">
                    <img :src="course.teacher.image">
                    <div class="name">
                      <p class="teacher-name">{{course.teacher.name}} {{course.teacher.title}}</p>
                      <p class="teacher-title">{{course.teacher.signature}}</p>
                    </div>
                  </div>
                  <p class="narrative">Linux运维技术专家,老男孩Linux金牌讲师,讲课风趣幽默、深入浅出、声音洪亮到爆炸</p>
                </div>
              </div>
            </div>
          </div>
        </div>
        <Footer/>
      </div>
    </template>
    
    <script>
      import Header from "./common/Header"
      import Footer from "./common/Footer"
      import {videoPlayer} from 'vue-video-player';
    
      export default {
        name: 'Detail',
        data() {
          return {
            token: sessionStorage.token || localStorage.token,
            user_id: sessionStorage.id || localStorage.id,
            user_name: sessionStorage.username || localStorage.username,
            tabIndex: 1,  // 当前选项卡显示的下标
            course_id: 0, // 当前页面对应的课程ID
            course: {
              teacher: {},
            },  // 课程详情信息
            chapter_list: {},
            playerOptions: {
              playbackRates: [0.7, 1.0, 1.5, 2.0], // 播放速度
              autoplay: false, //如果true,则自动播放
              muted: false, // 默认情况下将会消除任何音频。
              loop: false, // 循环播放
              preload: 'auto',  // 建议浏览器在<video>加载元素后是否应该开始下载视频数据。auto浏览器选择最佳行为,立即开始加载视频(如果浏览器支持)
              language: 'zh-CN',
              aspectRatio: '16:9', // 将播放器置于流畅模式,并在计算播放器的动态大小时使用该值。值应该代表一个比例 - 用冒号分隔的两个数字(例如"16:9"或"4:3")
              fluid: true, // 当true时,Video.js player将拥有流体大小。换句话说,它将按比例缩放以适应其容器。
              sources: [{ // 播放资源和资源格式
                type: "video/mp4",
                src: "http://img.ksbbs.com/asset/Mon_1703/05cacb4e02f9d9e.mp4" //你的视频地址(必填)
              }],
              poster: "../static/courses/675076.jpeg", //视频封面图
               document.documentElement.clientWidth, // 默认视频全屏时的最大宽度
              notSupportedMessage: '此视频暂无法播放,请稍后再试', //允许覆盖Video.js无法播放媒体源时显示的默认信息。
            },
    
          }
        },
        watch: {
          course(data) {
            this.playerOptions.sources[0].src = data.video;
            this.playerOptions.poster = data.course_img;
            while (data.brief.search(`"/media`) != -1) {
              data.brief = data.brief.replace(`"/media`, `"${this.$settings.Host}/media`)
            }
          },
          tabIndex(data) {
            if (data == 2) {
              //获取当前课程对应的章节列表和课时列表
              this.$axios.get(`${this.$settings.Host}/courses/chapters/?course=${this.course_id}`).then(response => {
                this.chapter_list = response.data;
              }).catch(error => {
                console.log(error.response)
              })
            }
          }
        },
        components: {
          Header,
          Footer,
          videoPlayer,
        },
        methods: {
          // 视频播放事件
          onPlayerPlay(player) {
            // alert("play");
          },
          // 视频暂停播放事件
          onPlayerPause(player) {
            // alert("pause");
          },
          // 视频插件初始化
          player() {
            return this.$refs.videoPlayer.player;
          },
          cartAddHander() {
            let token = sessionStorage.token || localStorage.token;
            if (!token) {
              console.log('confirm');
              this.$confirm('对不起,您尚未登录!请登录', '提示').then(() => {
                this.$router.push('/login');
              }).catch(()=>{
              });
              return;
    
            }
            this.$axios.post(this.$settings.Host + `/cart/course/`, {
              course_id: this.course_id,
            }, {
              headers: {
                'Authorization': 'jwt ' + this.token
              },
            }).then(response => {
              this.$store.state.cart.count = response.data.count;
              // this.$store.commit('addcart', response.data);
              this.$message({
                duration: 1000,
                showClose: true,
                message: response.data.message,
                type: 'success',
              })
            }).catch(error => {
              console.log(error.response);
            })
          },
        },
        created() {
    
          let id = Number(this.$route.query.id);
          this.course_id = id;
          console.log(id);
          console.log(typeof id);
          let _this = this;
          if (isNaN(id) || id < 1) {
            _this.$alert('无效的课程ID!', '错误', {
              callback() {
                _this.$router.go(-1);
              }
            });
          }
          this.$axios.get(this.$settings.Host + `/courses/detail/${this.course_id}/`).then(response => {
            this.course = response.data;
            this.playerOptions.poster = this.course.course_img;
          }).catch(error => {
            console.log(error.response)
          })
        },
      };
    </script>
    
    <style scoped>
      .main {
        background: #fff;
        padding-top: 30px;
      }
    
      .course-info {
        width: 1200px;
        margin: 0 auto;
        overflow: hidden;
      }
    
      .wrap-left {
        float: left;
        width: 690px;
        height: 388px;
        background-color: #000;
      }
    
      .wrap-right {
        float: left;
        position: relative;
        height: 388px;
      }
    
      .course-name {
        font-size: 20px;
        color: #333;
        padding: 10px 23px;
        letter-spacing: .45px;
      }
    
      .data {
        padding-left: 23px;
        padding-right: 23px;
        padding-bottom: 16px;
        font-size: 14px;
        color: #9b9b9b;
      }
    
      .sale-time {
        width: 464px;
        background: #fa6240;
        font-size: 14px;
        color: #4a4a4a;
        padding: 10px 23px;
        overflow: hidden;
      }
    
      .sale-type {
        font-size: 16px;
        color: #fff;
        letter-spacing: .36px;
        float: left;
      }
    
      .sale-time .expire {
        font-size: 14px;
        color: #fff;
        float: right;
      }
    
      .sale-time .expire .second {
        width: 24px;
        display: inline-block;
        background: #fafafa;
        color: #5e5e5e;
        padding: 6px 0;
        text-align: center;
      }
    
      .course-price {
        background: #fff;
        font-size: 14px;
        color: #4a4a4a;
        padding: 5px 23px;
      }
    
      .discount {
        font-size: 26px;
        color: #fa6240;
        margin-left: 10px;
        display: inline-block;
        margin-bottom: -5px;
      }
    
      .original {
        font-size: 14px;
        color: #9b9b9b;
        margin-left: 10px;
        text-decoration: line-through;
      }
    
      .buy {
        width: 464px;
        padding: 0px 23px;
        position: absolute;
        left: 0;
        bottom: 20px;
        overflow: hidden;
      }
    
      .buy .buy-btn {
        float: left;
      }
    
      .buy .buy-now {
        width: 125px;
        height: 40px;
        border: 0;
        background: #ffc210;
        border-radius: 4px;
        color: #fff;
        cursor: pointer;
        margin-right: 15px;
        outline: none;
      }
    
      .buy .free {
        width: 125px;
        height: 40px;
        border-radius: 4px;
        cursor: pointer;
        margin-right: 15px;
        background: #fff;
        color: #ffc210;
        border: 1px solid #ffc210;
      }
    
      .add-cart {
        float: right;
        font-size: 14px;
        color: #ffc210;
        text-align: center;
        cursor: pointer;
        margin-top: 10px;
      }
    
      .add-cart img {
        width: 20px;
        height: 18px;
        margin-right: 7px;
        vertical-align: middle;
      }
    
      .course-tab {
        width: 100%;
        background: #fff;
        margin-bottom: 30px;
        box-shadow: 0 2px 4px 0 #f0f0f0;
    
      }
    
      .course-tab .tab-list {
        width: 1200px;
        margin: auto;
        color: #4a4a4a;
        overflow: hidden;
      }
    
      .tab-list li {
        float: left;
        margin-right: 15px;
        padding: 26px 20px 16px;
        font-size: 17px;
        cursor: pointer;
      }
    
      .tab-list .active {
        color: #ffc210;
        border-bottom: 2px solid #ffc210;
      }
    
      .tab-list .free {
        color: #fb7c55;
      }
    
      .course-content {
        width: 1200px;
        margin: 0 auto;
        background: #FAFAFA;
        overflow: hidden;
        padding-bottom: 40px;
      }
    
      .course-tab-list {
        width: 880px;
        height: auto;
        padding: 20px;
        background: #fff;
        float: left;
        box-sizing: border-box;
        overflow: hidden;
        position: relative;
        box-shadow: 0 2px 4px 0 #f0f0f0;
      }
    
      .tab-item {
        width: 880px;
        background: #fff;
        padding-bottom: 20px;
        box-shadow: 0 2px 4px 0 #f0f0f0;
      }
    
      .tab-item-title {
        justify-content: space-between;
        padding: 25px 20px 11px;
        border-radius: 4px;
        margin-bottom: 20px;
        border-bottom: 1px solid #333;
        border-bottom-color: rgba(51, 51, 51, .05);
        overflow: hidden;
      }
    
      .chapter {
        font-size: 17px;
        color: #4a4a4a;
        float: left;
      }
    
      .chapter-length {
        float: right;
        font-size: 14px;
        color: #9b9b9b;
        letter-spacing: .19px;
      }
    
      .chapter-title {
        font-size: 16px;
        color: #4a4a4a;
        letter-spacing: .26px;
        padding: 12px;
        background: #eee;
        border-radius: 2px;
        display: -ms-flexbox;
        display: flex;
        -ms-flex-align: center;
        align-items: center;
      }
    
      .chapter-title img {
        width: 18px;
        height: 18px;
        margin-right: 7px;
        vertical-align: middle;
      }
    
      .lesson-list {
        padding: 0 20px;
      }
    
      .lesson-list .lesson-item {
        padding: 15px 20px 15px 36px;
        cursor: pointer;
        justify-content: space-between;
        position: relative;
        overflow: hidden;
      }
    
      .lesson-item .name {
        font-size: 14px;
        color: #666;
        float: left;
      }
    
      .lesson-item .index {
        margin-right: 5px;
      }
    
      .lesson-item .free {
        font-size: 12px;
        color: #fff;
        letter-spacing: .19px;
        background: #ffc210;
        border-radius: 100px;
        padding: 1px 9px;
        margin-left: 10px;
      }
    
      .lesson-item .time {
        font-size: 14px;
        color: #666;
        letter-spacing: .23px;
        opacity: 1;
        transition: all .15s ease-in-out;
        float: right;
      }
    
      .lesson-item .time img {
        width: 18px;
        height: 18px;
        margin-left: 15px;
        vertical-align: text-bottom;
      }
    
      .lesson-item .try {
        width: 86px;
        height: 28px;
        background: #ffc210;
        border-radius: 4px;
        font-size: 14px;
        color: #fff;
        position: absolute;
        right: 20px;
        top: 10px;
        opacity: 0;
        transition: all .2s ease-in-out;
        cursor: pointer;
        outline: none;
        border: none;
      }
    
      .lesson-item:hover {
        background: #fcf7ef;
        box-shadow: 0 0 0 0 #f3f3f3;
      }
    
      .lesson-item:hover .name {
        color: #333;
      }
    
      .lesson-item:hover .try {
        opacity: 1;
      }
    
      .course-side {
        width: 300px;
        height: auto;
        margin-left: 20px;
        float: right;
      }
    
      .teacher-info {
        background: #fff;
        margin-bottom: 20px;
        box-shadow: 0 2px 4px 0 #f0f0f0;
      }
    
      .side-title {
        font-weight: normal;
        font-size: 17px;
        color: #4a4a4a;
        padding: 18px 14px;
        border-bottom: 1px solid #333;
        border-bottom-color: rgba(51, 51, 51, .05);
      }
    
      .side-title span {
        display: inline-block;
        border-left: 2px solid #ffc210;
        padding-left: 12px;
      }
    
      .teacher-content {
        padding: 30px 20px;
        box-sizing: border-box;
      }
    
      .teacher-content .cont1 {
        margin-bottom: 12px;
        overflow: hidden;
      }
    
      .teacher-content .cont1 img {
        width: 54px;
        height: 54px;
        margin-right: 12px;
        float: left;
      }
    
      .teacher-content .cont1 .name {
        float: right;
      }
    
      .teacher-content .cont1 .teacher-name {
        width: 188px;
        font-size: 16px;
        color: #4a4a4a;
        padding-bottom: 4px;
      }
    
      .teacher-content .cont1 .teacher-title {
        width: 188px;
        font-size: 13px;
        color: #9b9b9b;
        white-space: nowrap;
      }
    
      .teacher-content .narrative {
        font-size: 14px;
        color: #666;
        line-height: 24px;
      }
    </style>
    View Code

    修复添加商品课程到购物车的前端BUG

    用户在课程详情页中退出登录时,如果不刷新页面,则可以继续进行购物。

    出现问题的原因:添加购物车时,判断用户是否登录依靠的是当前组件中data的token数据,而token数据是通过sessionStorage或者localStorage赋值过来的,但是当用户退出登录时,我们删除了sessionStorage或者localStorage,但是并没有删除token值。因为形成了BUG。

    解决方案:退出登录进行页面跳转.或者添加商品课程到购物车时,重新对token赋值。

    方案1:再header.vue中

    logout(){
    
            this.token = false;
            this.user_id=false;
            this.user_name=false;
    
            sessionStorage.removeItem("token");
            sessionStorage.removeItem("user_id");
            sessionStorage.removeItem("user_name");
    
            localStorage.removeItem("token");
            localStorage.removeItem("user_id");
            localStorage.removeItem("user_name");
            let _this = this;
            _this.$alert('退出登录成功!', '路飞学城', {
              callback(){
                _this.$router.push("/");
              }
            });

    方案2: 再Detail.vue中

     cartAddHander() {
            this.token = sessionStorage.token || localStorage.token;
            if (!this.token) {
              this.$confirm('对不起,您尚未登录!请登录', '提示').then(() => {
                this.$router.push('/login');
              }).catch(() => {
              });
              return;
    。。。。

    后端提供获取购物车课程信息

    from rest_framework.views import APIView
    from courses.models import Course
    from rest_framework.response import Response
    from django_redis import get_redis_connection
    from rest_framework.permissions import IsAuthenticated
    from rest_framework import status
    from django.conf import settings
    class CartAPIView(APIView):
        permission_classes = [IsAuthenticated]
        """购物车视图"""
        def get(self,request):
            """获取购物车商品课程列表"""
            # 获取当前用户ID
            # user_id = 1
            user_id = request.user.id
            # 通过用户ID获取购物车中的商品信息
            redis = get_redis_connection("cart")
            cart_goods_list = redis.hgetall("cart_%s" % user_id ) # 商品课程列表
            cart_goods_selects = redis.smembers("cart_selected_%s" % user_id)
            # redis里面的所有数据最终都是以bytes类型的字符串保存的
            # print( cart_goods_selects ) # 格式: {b'7', b'3', b'5'}
            # print( cart_goods_list ) # 格式: {b'7': b'-1', b'5': b'-1'}
            # 遍历购物车中的商品课程到数据库获取课程的价格, 标题, 图片
            data_list = []
            try:
                for course_id_bytes,expire_bytes in cart_goods_list.items():
                    course_id = int( course_id_bytes.decode() )
                    expire    = expire_bytes.decode()
                    course = Course.objects.get(pk=course_id)
                    data_list.append({
                        "id": course_id,
                        "expire":expire,
                        "course_img": course.course_img.url,
                        "name": course.name,
                        "price": course.get_course_price(),
                        "is_select": course_id_bytes in cart_goods_selects
                    })
            except:
                return Response(data_list,status=status.HTTP_500_INTERNAL_SERVER_ERROR)
            # print(data_list)
            # 返回查询结果
            return Response(data_list,status=status.HTTP_200_OK)
    
        def post(self,request):
            """购物车添加商品"""
            ....

    前端请求显示课程信息

    Cart.vue,代码:

    <template>
        <div class="cart">
          <Header/>
          <div class="main">
            <div class="cart-title">
              <h3>我的购物车 <span> 共2门课程</span></h3>
            </div>
            <div class="cart-info">
              <el-table 
                :data="courseData" 
                style="100%"
              >
                <el-table-column type="selection" label="" width="87"></el-table-column>
                <el-table-column label="课程" width="540">
                  <template slot-scope="scope">
                    <div class="course-box">
                      <img :src="$settings.Host + scope.row.course_img" alt="">
                      {{scope.row.name}}
                    </div>
                  </template>
                </el-table-column>
                <el-table-column label="有效期" width="216">
                  <template slot-scope="scope">
                    <el-form ref="form" label-width="60px">
                      <el-form-item>
                        <el-select v-model="scope.row.expire" placeholder="请选择有效期">
                          <el-option v-for="item in expire_list" :key="item.id" :label="item.title" :value="scope.row.id"></el-option>
                        </el-select>
                      </el-form-item>
                    </el-form>
                  </template>
                </el-table-column>
                <el-table-column label="单价" width="162">
                  <template slot-scope="scope">¥{{ scope.row.price.toFixed(2) }}</template>
                </el-table-column>
                <el-table-column label="操作" width="162">
                  <template slot-scope="scope">
                    <a @click="CartDel(scope.row.id,scope.row.name)">删除</a>
                  </template>
                </el-table-column>
              </el-table>
            </div>
            <div class="cart-bottom">
              <div class="select-all"><el-checkbox>全选</el-checkbox></div>
              <div class="delete-any"><img src="../../static/img/ico3.png" alt="">删除</div>
              <div class="cart-bottom-right">
                <span class="total">总计:¥<span>0.0</span></span>
                <span class="go-pay">去结算</span>
              </div>
            </div>
          </div>
          <Footer/>
        </div>
    </template>
    
    <script>
    import Header from "./common/Header"
    import Footer from "./common/Footer"
    export default {
        name: "Cart",
        data(){
          return{
            expire:3,
            expire_list:[
                {title:"一个月有效",id:1},
                {title:"两个月有效",id:2},
                {title:"三个月有效",id:3},
                {title:"永久有效",id:-1},
            ],
            courseData:[]
          }
        },
        created(){
          // 判断是否登录
          this.token = sessionStorage.token || localStorage.token;
          if( !this.token ){
            this.$confirm("对不起,您尚未登录!请登录",'提示').then(() => {
              this.$router.push("/login");
            }).catch(()=>{
              this.$router.go(-1);
            });
          }else{
            // 获取购物车商品数据
            this.$axios.get(this.$settings.Host+"/carts/course/",{
              headers:{
                // 注意下方的空格!!!
                "Authorization":"jwt " + this.token
              }
            }).then(response=>{
    
              this.courseData = response.data;
              // 更新在vuex里面的数据
              this.$store.state.cart.count = response.data.length;
            })
    
          }
        },
        methods:{
          CartDel(course_id,course_name){
            this.$confirm(`您确定要从购物车删除<<${course_name}>>这个课程么?`,"提示!").then(()=>{
              this.$message("删除成功!");
    
            }).catch(()=>{
              // 取消操作
    
            });
          }
        },
        components:{Header,Footer}
    }
    </script>
    View Code

    修复商品课程的默认选中和有效期的文本显示效果

    <template>
        <div class="cart">
          <Header/>
          <div class="main">
            <div class="cart-title">
              <h3>我的购物车 <span> 共2门课程</span></h3>
            </div>
            <div class="cart-info">
              <el-table
                :data="courseData"
                style="100%"
                ref="multipleTable"
              >
                <el-table-column type="selection" width="87"></el-table-column>
                <el-table-column label="课程" width="540">
                  <template slot-scope="scope">
                    <div class="course-box">
                      <img :src="$settings.Host + scope.row.course_img" alt="">
                      {{scope.row.name}}
                    </div>
                  </template>
                </el-table-column>
                <el-table-column label="有效期" width="216">
                  <template slot-scope="scope">
                    <el-select v-model="scope.row.expire" placeholder="请选择">
                      <el-option v-for="item in expire_list" :key="item.id" :label="item.title" :value="item.id"></el-option>
                    </el-select>
                  </template>
                </el-table-column>
                <el-table-column label="单价" width="162">
                  <template slot-scope="scope">¥{{ scope.row.price.toFixed(2) }}</template>
                </el-table-column>
                <el-table-column label="操作" width="162">
                  <template slot-scope="scope">
                    <a @click="CartDel(scope.row.id,scope.row.name)">删除</a>
                  </template>
                </el-table-column>
              </el-table>
            </div>
            <div class="cart-bottom">
              <div class="select-all"><el-checkbox>全选</el-checkbox></div>
              <div class="delete-any"><img src="../../static/img/ico3.png" alt="">删除</div>
              <div class="cart-bottom-right">
                <span class="total">总计:¥<span>0.0</span></span>
                <span class="go-pay">去结算</span>
              </div>
            </div>
          </div>
          <Footer/>
        </div>
    </template>
    
    <script>
    import Header from "./common/Header"
    import Footer from "./common/Footer"
    export default {
        name: "Cart",
        data(){
          return{
            expire:3,
            expire_list:[
                {title:"一个月有效",id:1},
                {title:"两个月有效",id:2},
                {title:"三个月有效",id:3},
                {title:"永久有效",id:-1},
            ],
            courseData:[]
        },
        created(){
          // 判断是否登录
          this.token = sessionStorage.token || localStorage.token;
          if( !this.token ){
            this.$confirm("对不起,您尚未登录!请登录",'提示').then(() => {
              this.$router.push("/login");
            }).catch(()=>{
              this.$router.go(-1);
            });
          }else{
            // 获取购物车商品数据
            this.$axios.get(this.$settings.Host+"/carts/course/",{
              headers:{
                // 注意下方的空格!!!
                "Authorization":"jwt " + this.token
              }
            }).then(response=>{
    
              this.courseData = response.data;
              // 更新在vuex里面的数据
              this.$store.state.cart.count = response.data.length;
    
              // 调整因为ajax数据请求导致勾选状态人没有出现的原因,使用定时器进行延时调用
              setTimeout(()=>{
                let expire_data = [];
                this.expire_list.forEach(row=>{
                  expire_data[row.id] = row.title;
                });
    
                // row 就是字典数据[json]
                this.courseData.forEach(row => {
                  // 设置商品课程的选中状态 ,加上延时处理
                  if(row.is_select){
                    this.$refs.multipleTable.toggleRowSelection(row);
                  }
    
                  // 调整有效期选项中数值变成文本内容
                  row.expire = expire_data[row.expire];
                });
              },0)
    
            }).catch(error=>{
              let status = error.response.status;
              if( status == 401 ){
                this.token = null;
                sessionStorage.removeItem("token");
                localStorage.removeItem("token");
                let _this = this;
                this.$alert("您尚未登录或登录超时!请重新登录","警告",{
                  callback(){
                    _this.$router.push("/login");
                  }
                });
              }
            })
    
          }
        },
        methods:{
          CartDel(course_id,course_name){
            this.$confirm(`您确定要从购物车删除<<${course_name}>>这个课程么?`,"提示!").then(()=>{
              this.$message("删除成功!");
    
            }).catch(()=>{
              // 取消操作
    
            });
          }
        },
        components:{Header,Footer}
    }
    View Code

    切换勾选状态

    后端提供修改勾选状态的接口

    视图代码:

    from django.shortcuts import render
    
    # Create your views here.
    from rest_framework.views import APIView
    from courses.models import Course
    from rest_framework.response import Response
    from django_redis import get_redis_connection
    from rest_framework.permissions import IsAuthenticated
    from rest_framework import status
    
    class CartAPIView(APIView):
        permission_classes = [IsAuthenticated]
        """购物车视图"""
        def get(self,request):
            """获取购物车商品课程列表"""
            # 获取当前用户ID
            # user_id = 1
            user_id = request.user.id
            # 通过用户ID获取购物车中的商品信息
            redis = get_redis_connection("cart")
            cart_goods_list = redis.hgetall("cart_%s" % user_id ) # 商品课程列表
            cart_goods_selects = redis.smembers("cart_selected_%s" % user_id)
            # redis里面的所有数据最终都是以bytes类型的字符串保存的
            # print( cart_goods_selects ) # 格式: {b'7', b'3', b'5'}
            # print( cart_goods_list ) # 格式: {b'7': b'-1', b'5': b'-1'}
            # 遍历购物车中的商品课程到数据库获取课程的价格, 标题, 图片
            data_list = []
            try:
                for course_id_bytes,expire_bytes in cart_goods_list.items():
                    course_id = int( course_id_bytes.decode() )
                    expire    = expire_bytes.decode()
                    course = Course.objects.get(pk=course_id)
                    data_list.append({
                        "id": course_id,
                        "expire":expire,
                        "course_img": course.course_img.url,
                        "name": course.name,
                        "price": course.get_course_price(),
                        "is_select": course_id_bytes in cart_goods_selects
                    })
            except:
                return Response(data_list,status=status.HTTP_500_INTERNAL_SERVER_ERROR)
            # print(data_list)
            # 返回查询结果
            return Response(data_list,status=status.HTTP_200_OK)
    
        def post(self,request):
            """购物车添加商品"""
            # 获取客户端发送过来的课程ID
            course_id = request.data.get("course_id")
            # 验证课程ID是否有效
            try:
                Course.objects.get(pk=course_id,is_delete=False,is_show=True)
            except Course.DoesNotExist:
                return Response({"message":"当前课程不存在!"},status=status.HTTP_400_BAD_REQUEST)
    
            # 组装基本数据[课程ID,有效期]保存到redis
            redis = get_redis_connection("cart")
            # user_id = 1
            user_id = request.user.id
            # transation: 事务
            # 作用: 可以设置多个数据库操作看成一个整体,这个整理里面每一条数据库操作都成功了,事务才算成功,
            #      如果出现其中任意一个数据库操作失败,则整体一起失败!
            # 事务可以提供 提交事务 和 回滚事务 的功能
            # 不仅mysql中存在事务,在redis中也有事务的概念,但是叫"管道 pipeline"
            try:
                # 创建事务[管道]对象
                pipeline = redis.pipeline()
                # 开启事务
                pipeline.multi()
                # 添加一个成员到指定名称的hash数据中[如果对应名称的hash数据不存在,则自动创建]
                # hset(名称,键,值)
                pipeline.hset("cart_%s" % user_id, course_id, -1) # -1表示购买的课程永久有效
    
                # 添加一个成员到制定名称的set数据中[如果对应名称的set数据不存在,则自动创建]
                # sadd(名称,成员)
                pipeline.sadd("cart_selected_%s" % user_id, course_id )
    
                # 提交事务[如果不提交,则事务会自动回滚]
                pipeline.execute()
    
            except:
                return Response({"message": "添加课程到购物车失败!请联系客服人员~"},status=status.HTTP_507_INSUFFICIENT_STORAGE)
    
            # 返回结果,返回购物车中的商品数量
            count = redis.hlen("cart_%s" % user_id)
    
            return Response({
                "message": "成功添加课程到购物车!",
                "count": count,
            }, status=status.HTTP_200_OK)
    
        def put(self,request):
            """购物车更新商品信息"""
            # 获取当前登录用户ID
            user_id = request.user.id
    
            # 接受课程ID,判断课程ID是否存在
            course_id = request.data.get("course_id")
            try:
                Course.objects.get(pk=course_id,is_delete=False,is_show=True)
            except Course.DoesNotExist:
                return Response({"message":"当前课程不存在!"},status=status.HTTP_400_BAD_REQUEST)
    
            # 获取勾选状态
            is_select = request.data.get("is_select")
    
            # 链接redis
            redis = get_redis_connection("cart")
    
            # 修改购物车中指定商品课程的信息
            if is_select:
                # 从勾选集合中新增一个课程ID
                redis.sadd("cart_selected_%s" % user_id, course_id )
            else:
                redis.srem("cart_selected_%s" % user_id, course_id )
    
            return Response({
                "message": "修改购物车信息成功!"
            }, status=status.HTTP_200_OK)
    View Code

    前端发送更新勾选状态的请求

    template代码:

              <el-table
                :data="courseData"
                style="100%"
                ref="multipleTable"
                @select="currentSelected"
              >

    script新增一个 操作方法:

    methods:{
          CartDel(course_id,course_name){
            ....
          },
          currentSelected(selection,row){
            // selection 表示所有被勾选的信息
            // row 当前操作的数据
            let is_select = true;
            if( selection.indexOf(row) == -1 ){
              is_select = false;
            }
    
            // 获取当前课程ID
            let course_id = row.id;
            // 切换勾选状态
    
            // 发送请求
            this.$axios.put(this.$settings.Host+"/carts/course/",{
              course_id: course_id,
              is_select: is_select,
            },{
              headers:{
                // 注意下方的空格!!!
                "Authorization":"jwt " + this.token
              },
            }).then(response=>{
    
              this.$message(response.data.message,"提示");
            }).catch(error=>{
    
              console.log(error.response)
            })
          }
        },
    View Code

    课程有效期

    后端提供修改勾选状态的接口

    因为前面实现修改购物车商品课程的勾选状态已经使用put方法提供API接口了,所以我们现在要修改课程有效期,业务上来说也是更新购物车中商品信息,所以我们可以继续在put里面通过判断是否有expire参数来执行不同代码,也可以使用http的patch操作来完成更新课程有效期的功能。

    我们这里使用patch方法。在视图代码中

        def patch(self,request):
            """更新购物城中的商品信息"""
            # 获取当前登录的用户ID
            # user_id = 1
            user_id = request.user.id
    
            # 获取当前操作的课程ID
            course_id = request.data.get("course_id")
    
            # 获取新的有效期
            expire = request.data.get("expire")
    
            # 获取redis链接
            redis = get_redis_connection("cart")
    
            # 更新购物中商品课程的有效期
            redis.hset("cart_%s" % user_id,course_id, expire)
    
            return Response({
                "message": "修改购物车信息成功!"
            }, status=status.HTTP_200_OK)
    View Code
    <script>
    import Header from "./common/Header"
    import Footer from "./common/Footer"
    export default {
        name: "Cart",
        data(){
          return{
            expire:3,
            expire_list:[
                {title:"一个月有效",id:1},
                {title:"两个月有效",id:2},
                {title:"三个月有效",id:3},
                {title:"永久有效",id:-1},
            ],
            courseData:[]
          }
        },
        created(){
          // 判断是否登录
          this.token = sessionStorage.token || localStorage.token;
          if( !this.token ){
            this.$confirm("对不起,您尚未登录!请登录",'提示').then(() => {
              this.$router.push("/login");
            }).catch(()=>{
              this.$router.go(-1);
            });
          }else{
            // 获取购物车商品数据
            this.$axios.get(this.$settings.Host+"/carts/course/",{
              headers:{
                // 注意下方的空格!!!
                "Authorization":"jwt " + this.token
              }
            }).then(response=>{
    
              this.courseData = response.data;
              // 更新在vuex里面的数据
              this.$store.state.cart.count = response.data.length;
    
              // 调整因为ajax数据请求导致勾选状态人没有出现的原因,使用定时器进行延时调用
              setTimeout(()=>{
                let expire_data = [];
                this.expire_list.forEach(row=>{
                  expire_data[row.id] = row.title;
                });
    
                // row 就是字典数据[json]
                this.courseData.forEach(row => {
                  // 设置商品课程的选中状态
                  if(row.is_select){
                    this.$refs.multipleTable.toggleRowSelection(row);
                  }
    
                  // 调整有效期选项中数值变成文本内容
                  row.expire = expire_data[row.expire];
                });
              },0)
    
            }).catch(error=>{
              let status = error.response.status;
              if( status == 401 ){
                this.token = null;
                sessionStorage.removeItem("token");
                localStorage.removeItem("token");
                let _this = this;
                this.$alert("您尚未登录或登录超时!请重新登录","警告",{
                  callback(){
                    _this.$router.push("/login");
                  }
                });
              }
            })
    
          }
        },
        methods:{
          CartDel(course,course_name){
            this.$confirm(`您确定要从购物车删除<<${course_name}>>这个课程么?`,"提示!").then(()=>{
              let course_id = course.id;
              // 发送请求
              this.$axios.delete(this.$settings.Host+"/carts/course/",{
                params:{
                  course_id:course_id,
                },
                headers:{
                  // 注意下方的空格!!!
                  "Authorization":"jwt " + this.token
                },
              }).then(response=>{
                let index = this.courseData.indexOf(course);
                this.courseData.splice(index,1);
                console.log(this.courseData);
                this.$message("删除成功!");
    
              }).catch(error=>{
                console.log(error.response);
              });
    
            }).catch(()=>{
              // 取消操作
    
            });
          },
          currentSelected(selection,row){
            // selection 表示所有被勾选的信息
            // row 当前操作的数据
            let is_select = true;
            if( selection.indexOf(row) == -1 ){
              is_select = false;
            }
    
            // 获取当前课程ID
            let course_id = row.id;
            // 切换勾选状态
    
            // 发送请求
            this.$axios.put(this.$settings.Host+"/carts/course/",{
              course_id: course_id,
              is_select: is_select,
            },{
              headers:{
                // 注意下方的空格!!!
                "Authorization":"jwt " + this.token
              },
            }).then(response=>{
    
              this.$message(response.data.message,"提示");
            }).catch(error=>{
    
              console.log(error.response)
            })
          },
          // 更新课程的有效期
          ChangeExpire(course){
            // 获取课程ID和有效期
            let course_id = course.id;
            let expire    = course.expire
            // 发送patch请求更新有效期
            this.$axios.patch(this.$settings.Host+"/carts/course/",{
              course_id,
              expire,  // 这里是简写,相当于 expire:expire,
            },{
              headers:{
                // 注意下方的空格!!!
                "Authorization":"jwt " + this.token
              },
            }).then(response=>{
              this.$message(response.data.message,"提示");
            });
          }
        },
        components:{Header,Footer}
    }
    </script>
    View Code

    删除购物车中的商品信息

    后端提供从购物车中删除商品课程的API接口

        def delete(self,request):
            """从购物车中删除数据"""
            # 获取当前登录用户ID
            # user_id = 1
            user_id = request.user.id
            # 获取课程ID
            course_id = request.query_params.get("course_id")
    
            redis = get_redis_connection("cart")
            pipeline = redis.pipeline()
    
            pipeline.multi()
            # 从购物车中删除指定商品课程
            pipeline.hdel("cart_%s" % user_id, course_id)
    
            # 从勾选集合中移除指定商品课程
            pipeline.srem("cart_selected_%s" % user_id, course_id )
    
            pipeline.execute()
    
            # 返回操作结果
            return Response({"message":"删除商品课程成功!"},status=status.HTTP_204_NO_CONTENT)
    View Code

    前端请求删除商品课程

    tempalte,代码:

    <template slot-scope="scope">
    <a @click="CartDel(scope.row,scope.row.name)">删除</a>
     </template>

    script标签代码:

    methods:{
          CartDel(course,course_name){
            this.$confirm(`您确定要从购物车删除<<${course_name}>>这个课程么?`,"提示!").then(()=>{
              let course_id = course.id;
              // 发送请求
              this.$axios.delete(this.$settings.Host+"/carts/course/",{
                params:{
                  course_id:course_id,
                },
                headers:{
                  // 注意下方的空格!!!
                  "Authorization":"jwt " + this.token
                },
              }).then(response=>{
                let index = this.courseData.indexOf(course);
                this.courseData.splice(index,1);
                console.log(this.courseData);
                this.$message("删除成功!");
    
              }).catch(error=>{
                console.log(error.response);
              });
    
            }).catch(()=>{
              // 取消操作
    
            });
          },
    View Code

    购物车的价格统计

    <template>
        <div class="cart">
          <Header/>
          <div class="main">
            <div class="cart-title">
              <h3>我的购物车 <span> 共2门课程</span></h3>
            </div>
            <div class="cart-info">
              <el-table
                :data="courseData"
                style="100%"
                ref="multipleTable"
                @select="currentSelected"
                @selection-change="SelectionChange"
              >
                <el-table-column type="selection" width="87"></el-table-column>
                <el-table-column label="课程" width="540">
                  <template slot-scope="scope">
                    <div class="course-box">
                      <img :src="$settings.Host + scope.row.course_img" alt="">
                      <router-link :to="'/detail?id='+scope.row.id">{{scope.row.name}}</router-link>
                    </div>
                  </template>
                </el-table-column>
                <el-table-column label="有效期" width="216">
                  <template slot-scope="scope">
                    <el-select @change="ChangeExpire(scope.row)" v-model="scope.row.expire" placeholder="请选择">
                      <el-option v-for="item in expire_list" :key="item.id" :label="item.title" :value="item.id"></el-option>
                    </el-select>
                  </template>
                </el-table-column>
                <el-table-column label="单价" width="162">
                  <template slot-scope="scope">¥{{ scope.row.price.toFixed(2) }}</template>
                </el-table-column>
                <el-table-column label="操作" width="162">
                  <template slot-scope="scope">
                    <a @click="CartDel(scope.row,scope.row.name)">删除</a>
                  </template>
                </el-table-column>
              </el-table>
            </div>
            <div class="cart-bottom">
              <div class="select-all"><el-checkbox>全选</el-checkbox></div>
              <div class="delete-any"><img src="../../static/img/ico3.png" alt="">删除</div>
              <div class="cart-bottom-right">
                <span class="total">总计:¥<span>{{total_price}}</span></span>
                <span class="go-pay">去结算</span>
              </div>
            </div>
          </div>
          <Footer/>
        </div>
    </template>
    
    <script>
    import Header from "./common/Header"
    import Footer from "./common/Footer"
    export default {
        name: "Cart",
        data(){
          return{
            expire:3,
            expire_list:[
                {title:"一个月有效",id:1},
                {title:"两个月有效",id:2},
                {title:"三个月有效",id:3},
                {title:"永久有效",id:-1},
            ],
            courseData:[], // 购物车中的商品信息
            selection:[],  // 购物车中被勾选的商品信息
            total_price:0.00,
          }
        },
        watch:{
          selection(){
            // 当课程勾选状态发生变化时核算价格
            this.getTotalPrice();
          },
          courseData(){
            // 当课程数量发生变化时核算价格
            this.getTotalPrice();
          },
        },
        created(){
          // 判断是否登录
          this.token = sessionStorage.token || localStorage.token;
          if( !this.token ){
            this.$confirm("对不起,您尚未登录!请登录",'提示').then(() => {
              this.$router.push("/login");
            }).catch(()=>{
              this.$router.go(-1);
            });
          }else{
            // 获取购物车商品数据
            this.$axios.get(this.$settings.Host+"/carts/course/",{
              headers:{
                // 注意下方的空格!!!
                "Authorization":"jwt " + this.token
              }
            }).then(response=>{
    
              this.courseData = response.data;
              // 更新在vuex里面的数据
              this.$store.state.cart.count = response.data.length;
    
              // 调整因为ajax数据请求导致勾选状态人没有出现的原因,使用定时器进行延时调用
              setTimeout(()=>{
                let expire_data = [];
                this.expire_list.forEach(row=>{
                  expire_data[row.id] = row.title;
                });
    
                // row 就是字典数据[json]
                this.courseData.forEach(row => {
                  // 设置商品课程的选中状态
                  if(row.is_select){
                    this.$refs.multipleTable.toggleRowSelection(row);
                  }
    
                  // 调整有效期选项中数值变成文本内容
                  row.expire = expire_data[row.expire];
                });
              },0)
    
            }).catch(error=>{
              let status = error.response.status;
              if( status == 401 ){
                this.token = null;
                sessionStorage.removeItem("token");
                localStorage.removeItem("token");
                let _this = this;
                this.$alert("您尚未登录或登录超时!请重新登录","警告",{
                  callback(){
                    _this.$router.push("/login");
                  }
                });
              }
            })
    
          }
        },
        methods:{
          getTotalPrice(){
            // 核算购物车中所有勾选商品的总价格
            let total = 0;
            this.selection.forEach(row=>{
              total += row.price;
            });
            // 保留2个小数位
            this.total_price = total.toFixed(2);
          },
          CartDel(course,course_name){
            this.$confirm(`您确定要从购物车删除<<${course_name}>>这个课程么?`,"提示!").then(()=>{
              let course_id = course.id;
              // 发送请求
              this.$axios.delete(this.$settings.Host+"/carts/course/",{
                params:{
                  course_id:course_id,
                },
                headers:{
                  // 注意下方的空格!!!
                  "Authorization":"jwt " + this.token
                },
              }).then(response=>{
                let index = this.courseData.indexOf(course);
                this.courseData.splice(index,1);
                this.$message("删除成功!");
    
              }).catch(error=>{
                console.log(error.response);
              });
    
            }).catch(()=>{
              // 取消操作
    
            });
          },
         Selected(selection,row){
            // selection 表示所有被勾选的信息
            // row 当前操作的数据
            let is_select = true;
            if( selection.indexOf(row) == -1 ){
              is_select = false;
            }
    
            // 获取当前课程ID
            let course_id = row.id;
            // 切换勾选状态
    
            // 发送请求
            this.$axios.put(this.$settings.Host+"/carts/course/",{
              course_id: course_id,
              is_select: is_select,
            },{
              headers:{
                // 注意下方的空格!!!
                "Authorization":"jwt " + this.token
              },
            }).then(response=>{
    
              this.$message(response.data.message,"提示");
            }).catch(error=>{
    
              console.log(error.response)
            })
          },
          // 更新课程的有效期
          ChangeExpire(course){
            // 获取课程ID和有效期
            let course_id = course.id;
            let expire    = course.expire
            // 发送patch请求更新有效期
            this.$axios.patch(this.$settings.Host+"/carts/course/",{
              course_id,
              expire,  // 这里是简写,相当于 expire:expire,
            },{
              headers:{
                // 注意下方的空格!!!
                "Authorization":"jwt " + this.token
              },
            }).then(response=>{
              this.$message(response.data.message,"提示");
            });
          },
          // 获取勾选过的商品课程列表
          SelectionChange(data){
            this.selection = data;
          }
        },
        components:{Header,Footer}
    }
    </script>
    View Code

    显示当前课程所属的真实价格

    价格策略模型

    价格优惠活动类型名称: 限时免费, 限时折扣, 限时减免, 积分抵扣, 满减, 优惠券
    公式:
    限时免费 原价-原价
    限时折扣 原价*0.8
    限时减免 原价-减免价
    积分抵扣 原价-(积分计算后换算价格) ->> 积分换算比率
    满减 原价-(满减计算后换算价格)
    优惠券 原价-优惠券价格 -->> 优惠券

     模型代码:

    from django.db import models
    from luffy.utils.models import BaseModel
    from datetime import datetime
    from decimal import Decimal
    # Create your models here.
    class CourseCategory(BaseModel):
        """
        课程分类
        """
        name = models.CharField(max_length=64, unique=True, verbose_name="分类名称")
        class Meta:
            db_table = "ly_course_category"
            verbose_name = "课程分类"
            verbose_name_plural = "课程分类"
    
        def __str__(self):
            return "%s" % self.name
    
    
    from ckeditor_uploader.fields import RichTextUploadingField
    class Course(BaseModel):
        """
        专题课程
        """
        course_type = (
            (0, '付费'),
            (1, 'VIP专享'),
            (2, '学位课程')
        )
        level_choices = (
            (0, '初级'),
            (1, '中级'),
            (2, '高级'),
        )
        status_choices = (
            (0, '上线'),
            (1, '下线'),
            (2, '预上线'),
        )
        name = models.CharField(max_length=128, verbose_name="课程名称")
        course_img = models.ImageField(upload_to="course", max_length=255, verbose_name="封面图片", blank=True, null=True)
        course_type = models.SmallIntegerField(choices=course_type,default=0, verbose_name="付费类型")
        # 使用这个字段的原因
        video = models.FileField(upload_to="video", null=True,blank=True,default=None, verbose_name="封面视频")
        brief = RichTextUploadingField(max_length=2048, verbose_name="详情介绍", null=True, blank=True)
        level = models.SmallIntegerField(choices=level_choices, default=1, verbose_name="难度等级")
        pub_date = models.DateField(verbose_name="发布日期", auto_now_add=True)
        period = models.IntegerField(verbose_name="建议学习周期(day)", default=7)
        attachment_path = models.FileField(max_length=128, verbose_name="课件路径", blank=True, null=True)
        status = models.SmallIntegerField(choices=status_choices, default=0, verbose_name="课程状态")
        course_category = models.ForeignKey("CourseCategory", on_delete=models.CASCADE, null=True, blank=True,verbose_name="课程分类")
        students = models.IntegerField(verbose_name="学习人数",default = 0)
        lessons = models.IntegerField(verbose_name="总课时数量",default = 0)
        pub_lessons = models.IntegerField(verbose_name="课时更新数量",default = 0)
        price = models.DecimalField(max_digits=6,decimal_places=2, verbose_name="课程原价",default=0)
        teacher = models.ForeignKey("Teacher",on_delete=models.DO_NOTHING, null=True, blank=True,verbose_name="授课老师")
        class Meta:
            db_table = "ly_course"
            verbose_name = "专题课程"
            verbose_name_plural = "专题课程"
    
        def __str__(self):
            return "%s" % self.name
    
        def get_course_discount_type(self):
            now = datetime.now()
            try:
                course_prices = self.prices.get(start_time__lte=now, end_time__gte=now, is_delete=False, is_show=True)
                # 获取优惠活动类型
                return course_prices.discount.discount_type.name
            except:
                return ""
    
        def get_course_price(self):
            # 获取当前课程的真实价格
            # 获取当前课程的价格策略
    
            now = datetime.now()
            try:
                course_prices = self.prices.get( start_time__lte=now,end_time__gte=now, is_delete=False,is_show=True)
    
                # 价格优惠条件判断,原价大于优惠条件才参与活动
                if self.price < course_prices.discount.condition:
                    return self.price
    
                # 当优惠公式为多行文本,则表示满减
                if course_prices.discount.sale[0]=="":
                    sale_list = course_prices.discount.sale.split("
    ")
                    sale_price_list = []
                    # 通过遍历提取所有策略项的优惠价格值
                    for sale_item in sale_list:
                        sale = int( sale_item[1: sale_item.index("-")] )
                        sele_price = int( sale_item[sale_item.index("-")+1:] )
                        if self.price >= sale:
                            sale_price_list.append( sele_price )
    
                    # 当前课程只能享受一个最大优惠
                    return self.price - max( sale_price_list )
    
                # 当优惠公式为-1,则表示真实价格为0
                if course_prices.discount.sale == "-1":
                    return 0
    
                # 当优惠公式为*开头,则表示折扣
                if course_prices.discount.sale[0] == "*":
                    sale = Decimal( course_prices.discount.sale[1:] )
                    return self.price * sale
    
                # 当优惠公式为负数,则表示减免
                if course_prices.discount.sale[0] == "-":
                    sale = Decimal( course_prices.discount.sale[1:] )
                    real_price = self.price - sale
                    return real_price if real_price > 0 else 0
    
            except:
                print("---没有优惠---")
    
            return self.price
    
        def lesson_list(self):
            """获取当前课程的前8个课时展示到列表中"""
    
            # 获取所有章节
            chapters_list = self.coursechapters.filter(is_delete=False,is_show=True)
            lesson_list = []
            if chapters_list:
                for chapter in chapters_list:
                    lessons = chapter.coursesections.filter(is_delete=False,is_show=True)[:4]
                    if lessons:
                        for lesson in lessons:
                            lesson_list.append({
                                "id":lesson.id,
                                "name":lesson.name,
                                "free_trail":lesson.free_trail
                            })
    
            return lesson_list[:4]
    
        def course_level(self):
            """把课程难度数值转换成文本"""
            return self.level_choices[self.level][1]
    
    class Teacher(BaseModel):
        """讲师、导师表"""
        role_choices = (
            (0, '讲师'),
            (1, '导师'),
            (2, '班主任'),
        )
        name = models.CharField(max_length=32, verbose_name="讲师title")
        role = models.SmallIntegerField(choices=role_choices, default=0, verbose_name="讲师身份")
        title = models.CharField(max_length=64, verbose_name="职位、职称")
        signature = models.CharField(max_length=255, verbose_name="导师签名", help_text="导师签名", blank=True, null=True)
        image = models.ImageField(upload_to="teacher", null=True, blank=True, verbose_name = "讲师封面")
        brief = models.TextField(max_length=1024, verbose_name="讲师描述")
    
        class Meta:
            db_table = "ly_teacher"
            verbose_name = "讲师导师"
            verbose_name_plural = "讲师导师"
    
        def __str__(self):
            return "%s" % self.name
    
    
    class CourseChapter(BaseModel):
        """课程章节"""
        course = models.ForeignKey("Course", related_name='coursechapters', on_delete=models.CASCADE, verbose_name="课程名称")
        chapter = models.SmallIntegerField(verbose_name="第几章", default=1)
        name = models.CharField(max_length=128, verbose_name="章节标题")
        summary = models.TextField(verbose_name="章节介绍", blank=True, null=True)
        pub_date = models.DateField(verbose_name="发布日期", auto_now_add=True)
    
        class Meta:
            db_table = "ly_course_chapter"
            verbose_name = "课程章节"
            verbose_name_plural = "课程章节"
    
        def __str__(self):
            return "%s:(第%s章)%s" % (self.course, self.chapter, self.name)
    
    
    class CourseLesson(BaseModel):
        """课程课时"""
        section_type_choices = (
            (0, '文档'),
            (1, '练习'),
            (2, '视频')
        )
        chapter = models.ForeignKey("CourseChapter", related_name='coursesections', on_delete=models.CASCADE,verbose_name="课程章节")
        name = models.CharField(max_length=128,verbose_name = "课时标题")
        orders = models.PositiveSmallIntegerField(verbose_name="课时排序")
        section_type = models.SmallIntegerField(default=2, choices=section_type_choices, verbose_name="课时种类")
        section_link = models.CharField(max_length=255, blank=True, null=True, verbose_name="课时链接", help_text = "若是video,填vid,若是文档,填link")
        duration = models.CharField(verbose_name="视频时长", blank=True, null=True, max_length=32)  # 仅在前端展示使用
        pub_date = models.DateTimeField(verbose_name="发布时间", auto_now_add=True)
        free_trail = models.BooleanField(verbose_name="是否可试看", default=False)
    
        class Meta:
            db_table = "ly_course_lesson"
            verbose_name = "课程课时"
            verbose_name_plural = "课程课时"
    
        def __str__(self):
            return "%s-%s" % (self.chapter, self.name)
    
    """价格相关的模型"""
    class PriceDiscountType(BaseModel):
        """课程优惠类型"""
        name = models.CharField(max_length=32, verbose_name="类型名称")
        remark = models.CharField(max_length=250,blank=True, null=True, verbose_name="备注信息")
    
        class Meta:
            db_table = "ly_price_discount_type"
            verbose_name = "课程优惠类型"
            verbose_name_plural = "课程优惠类型"
    
        def __str__(self):
            return "%s" % (self.name)
    
    class PriceDiscount(BaseModel):
        """课程优惠模型"""
        discount_type = models.ForeignKey("PriceDiscountType",on_delete=models.CASCADE, related_name='pricediscounts',verbose_name="优惠类型")
        discount_name = models.CharField(max_length=150,verbose_name="优惠活动名称")
        condition     = models.IntegerField(blank=True,default=0,verbose_name="满足优惠的价格条件")
        sale          = models.TextField(verbose_name="优惠公式",help_text="""
        -1表示免费;<br>
        *号开头表示折扣价,例如*0.82表示八二折;<br>
        $号开头表示积分兑换,例如$50表示可以兑换50积分<br>
        表示满减,则需要使用 原价-优惠价格,例如表示,课程价格大于100,优惠10;大于200,优惠20,格式如下:<br>
        &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;满100-10<br>
        &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;满200-20<br>
               
        """)
    
        class Meta:
            db_table = "ly_price_discount"
            verbose_name = "价格优惠策略"
            verbose_name_plural = "价格优惠策略"
    
        def __str__(self):
            return "价格优惠:%s,优惠条件:%s,优惠值:%s" % (self.discount_type.name,self.condition,self.sale)
    
    class CoursePriceDiscount(BaseModel):
        """课程与优惠策略的关系表"""
        course = models.ForeignKey("Course",on_delete=models.CASCADE, related_name="prices",verbose_name="课程")
        discount = models.ForeignKey("PriceDiscount",on_delete=models.CASCADE,related_name="courses",verbose_name="优惠活动")
        start_time = models.DateTimeField(verbose_name="优惠策略的开始时间")
        end_time = models.DateTimeField(verbose_name="优惠策略的结束时间")
    
        class Meta:
            db_table = "ly_course_price_dicount"
            verbose_name="课程与优惠策略的关系表"
            verbose_name_plural="课程与优惠策略的关系表"
    
        def __str__(self):
            return "优惠: %s,开始时间:%s,结束时间:%s" % (self.discount.discount_name, self.start_time,self.end_time)
    View Code

    执行数据迁移

    python manage.py makemigrations
    python manage.py migrate

    后端在模型中计算课程真实价格

    因为课程的优惠是具有时效性的,所以我们计算价格的时候需要先判断当前优惠是否过期了。

    settings/dev.py,代码:USE_TZ = False

    courses/models.py,代码:

    from django.db import models
    from luffy.utils.models import BaseModel
    from datetime import datetime
    from decimal import Decimal
    # Create your models here.
    class CourseCategory(BaseModel):
        """
        课程分类
        """
        。。。
    
    
    from ckeditor_uploader.fields import RichTextUploadingField
    class Course(BaseModel):
        """
        专题课程
        """
        course_type = (
            (0, '付费'),
            (1, 'VIP专享'),
            (2, '学位课程')
        )
        level_choices = (
            (0, '初级'),
            (1, '中级'),
            (2, '高级'),
        )
        status_choices = (
            (0, '上线'),
            (1, '下线'),
            (2, '预上线'),
        )
        name = models.CharField(max_length=128, verbose_name="课程名称")
        course_img = models.ImageField(upload_to="course", max_length=255, verbose_name="封面图片", blank=True, null=True)
        course_type = models.SmallIntegerField(choices=course_type,default=0, verbose_name="付费类型")
        # 使用这个字段的原因
        video = models.FileField(upload_to="video", null=True,blank=True,default=None, verbose_name="封面视频")
        brief = RichTextUploadingField(max_length=2048, verbose_name="详情介绍", null=True, blank=True)
        level = models.SmallIntegerField(choices=level_choices, default=1, verbose_name="难度等级")
        pub_date = models.DateField(verbose_name="发布日期", auto_now_add=True)
        period = models.IntegerField(verbose_name="建议学习周期(day)", default=7)
        attachment_path = models.FileField(max_length=128, verbose_name="课件路径", blank=True, null=True)
        status = models.SmallIntegerField(choices=status_choices, default=0, verbose_name="课程状态")
        course_category = models.ForeignKey("CourseCategory", on_delete=models.CASCADE, null=True, blank=True,verbose_name="课程分类")
        students = models.IntegerField(verbose_name="学习人数",default = 0)
        lessons = models.IntegerField(verbose_name="总课时数量",default = 0)
        pub_lessons = models.IntegerField(verbose_name="课时更新数量",default = 0)
        price = models.DecimalField(max_digits=6,decimal_places=2, verbose_name="课程原价",default=0)
        teacher = models.ForeignKey("Teacher",on_delete=models.DO_NOTHING, null=True, blank=True,verbose_name="授课老师")
        class Meta:
            db_table = "ly_course"
            verbose_name = "专题课程"
            verbose_name_plural = "专题课程"
    
        def __str__(self):
            return "%s" % self.name
    
        def get_course_discount_type(self):
            now = datetime.now()
            try:
                course_prices = self.prices.get(start_time__lte=now, end_time__gte=now, is_delete=False, is_show=True)
                # 获取优惠活动类型
                return course_prices.discount.discount_type.name
            except:
                return ""
    
        def get_course_price(self):
            # 获取当前课程的真实价格
            # 获取当前课程的价格策略
    
            now = datetime.now()
            try:
                course_prices = self.prices.get( start_time__lte=now,end_time__gte=now, is_delete=False,is_show=True)
    
                # 价格优惠条件判断,原价大于优惠条件才参与活动
                if self.price < course_prices.discount.condition:
                    return self.price
    
                # 当优惠公式为多行文本,则表示满减
                if course_prices.discount.sale[0]=="":
                    sale_list = course_prices.discount.sale.split("
    ")
                    sale_price_list = []
                    # 通过遍历提取所有策略项的优惠价格值
                    for sale_item in sale_list:
                        sale = int( sale_item[1: sale_item.index("-")] )
                        sele_price = int( sale_item[sale_item.index("-")+1:] )
                        if self.price >= sale:
                            sale_price_list.append( sele_price )
    
                    # 当前课程只能享受一个最大优惠
                    return self.price - max( sale_price_list )
    
                # 当优惠公式为-1,则表示真实价格为0
                if course_prices.discount.sale == "-1":
                    return 0
    
                # 当优惠公式为*开头,则表示折扣
                if course_prices.discount.sale[0] == "*":
                    sale = Decimal( course_prices.discount.sale[1:] )
                    return self.price * sale
    
                # 当优惠公式为负数,则表示减免
                if course_prices.discount.sale[0] == "-":
                    sale = Decimal( course_prices.discount.sale[1:] )
                    real_price = self.price - sale
                    return real_price if real_price > 0 else 0
    
            except:
                print("---没有优惠---")
    
            return self.price
    
        def lesson_list(self):
            """获取当前课程的前8个课时展示到列表中"""
    
            # 获取所有章节
            chapters_list = self.coursechapters.filter(is_delete=False,is_show=True)
            lesson_list = []
            if chapters_list:
                for chapter in chapters_list:
                    lessons = chapter.coursesections.filter(is_delete=False,is_show=True)[:4]
                    if lessons:
                        for lesson in lessons:
                            lesson_list.append({
                                "id":lesson.id,
                                "name":lesson.name,
                                "free_trail":lesson.free_trail
                            })
    
            return lesson_list[:4]
    
        def course_level(self):
            """把课程难度数值转换成文本"""
            return self.level_choices[self.level][1]
    
    class Teacher(BaseModel):
        """讲师、导师表"""
        。。。
    
    
    class CourseChapter(BaseModel):
        """课程章节"""
        。。。
    
    
    class CourseLesson(BaseModel):
        """课程课时"""
        。。。
    
    """价格相关的模型"""
    class PriceDiscountType(BaseModel):
        """课程优惠类型"""
        。。。
    
    class PriceDiscount(BaseModel):
        """课程优惠模型"""
        。。。
    
    class CoursePriceDiscount(BaseModel):
        """课程与优惠策略的关系表"""
        。。。
    View Code


    修改序列化器,增加返回字段[优惠类型和优惠策略]

    from rest_framework import serializers
    from .models import CourseCategory,Course
    class CourseCategoryModelSerializer(serializers.ModelSerializer):
        class Meta:
            model = CourseCategory
            fields = ("id","name")
    
    from .models import Teacher
    class TeacherModelSerializer(serializers.ModelSerializer):
        class Meta:
            model = Teacher
            fields = ("id","name","title")
    
    class CourseModelSerializer(serializers.ModelSerializer):
           """课程列表页的序列化器"""
        # 默认情况,序列化器转换模型数据时,默认会把外键直接转成主键ID值
        # 所以我们需要重新设置在序列化器中针对外键的序列化
        # 这种操作就是一个序列器里面调用另一个序列化器了.叫"序列化器嵌套"
        teacher = TeacherModelSerializer()
        # coursechapters = CourseChapterModelSerializer(many=True)
        class Meta:
            model = Course
            fields = ("id","name","course_img","students","lessons","pub_lessons","price","teacher","lesson_list","get_course_price","get_course_discount_type")
    
    
    class TeacherDetailModelSerializer(serializers.ModelSerializer):
        。。。。
    
    
    class CourseDetailModelSerializer(serializers.ModelSerializer):
        """课程详情页的序列化器"""
        teacher = TeacherDetailModelSerializer()
        class Meta:
            model = Course
            # fields = ("id","name", "video", "course_img", "students","lessons","pub_lessons","price","teacher","course_level","brief")
            fields = ("id","name", "course_img", "students","lessons","pub_lessons","price","teacher","course_level","brief","get_course_price","get_course_discount_type")
    View Code

    前端课程列表页展示真实课程价格

    判断是否有课程类型,如果有,则显示优惠价格。没有则显示课程原价。

    <template>
      <div class="course">
        <Header/>
        <div class="main">
          <!-- 筛选功能 -->
          <div class="top">
            <ul class="condition condition1">
              <li class="cate-condition">课程分类:</li>
              <li class="item" :class="query_params.course_category==0?'current':''" @click="query_params.course_category=0">全部</li>
              <li :class="query_params.course_category==catetory.id?'current':''" @click="query_params.course_category=catetory.id" v-for="catetory in catetory_list" :data-key="catetory.id" class="item">{{catetory.name}}</li>
            </ul>
            <ul class="condition condition2">
              <li class="cate-condition">&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;选:</li>
              <li class="item" :class="(query_params.ordering=='-id' || query_params.ordering=='id')?'current':''" @click="select_ordering('id')">默认</li>
              <li class="item" :class="(query_params.ordering=='-students' || query_params.ordering=='students')?'current':''" @click="select_ordering('students')">人气</li>
              <li class="item" :class="query_params.ordering=='price'?'current price':(query_params.ordering=='-price'?'current price2':'')" @click="select_ordering('price')">价格</li>
              <li class="course-length">共21个课程</li>
            </ul>
          </div>
          <!-- 课程列表 --->
          <div class="list">
            <ul>
              <li class="course-item" v-for="course in course_list">
                <router-link :to="{path: '/detail',query:{id:course.id}}" class="course-link">
                  <div class="course-cover">
                    <img :src="course.course_img" alt="">
                  </div>
                  <div class="course-info">
                    <div class="course-title">
                      <h3>{{course.name}}</h3>
                      <span>{{course.students}}人已加入学习</span>
                    </div>
                    <p class="teacher">
                      <span class="info">{{course.teacher.name}} {{course.teacher.title}}</span>
                      <span class="lesson">共{{course.lessons}}课时/{{course.lessons==course.pub_lessons?'更新完成':('已更新'+course.pub_lessons+"课时")}}</span>
                    </p>
                    <ul class="lesson-list">
                      <li v-for="lesson,key in course.lesson_list">
                        <p class="lesson-title">0{{key+1}} | {{lesson.name}}</p>
                        <span v-if="lesson.free_trail" class="free">免费</span>
                      </li>
    
                    </ul>
                    <div class="buy-info">
                      <div v-if="course.get_course_discount_type">
                      <span class="discount">{{course.get_course_discount_type}}</span>
                      <span class="present-price">¥{{course.get_course_price}}元</span>
                      <span class="original-price">原价:{{course.price}}元</span>
                      </div>
                      <span v-else class="present-price">¥{{course.price}}元</span>
                      <button class="buy-now">立即购买</button>
                    </div>
                  </div>
                </router-link>
              </li>
            </ul>
          </div>
          <div class="pagination">
            <el-pagination
              @current-change="handleCurrentChange"
              :current-page="query_params.current_page"
              background
              layout="prev, pager, next"
              :page-size="course_page_size"
              :total="course_count">
            </el-pagination>
          </div>
        </div>
        <Footer/>
      </div>
    </template>
    View Code

    前端课程详情页展示真实课程的价格

    <template>
        <div class="detail">
          <Header/>
          <div class="main">
            <div class="course-info">
              <div class="wrap-left">
                <video-player class="video-player vjs-custom-skin"
                   ref="videoPlayer"
                   :playsinline="true"
                   :options="playerOptions"
                   @play="onPlayerPlay($event)"
                   @pause="onPlayerPause($event)"
                >
                </video-player>
              </div>
              <div class="wrap-right">
                <h3 class="course-name">{{course.name}}</h3>
                <p class="data">{{course.students}}人在学&nbsp;&nbsp;&nbsp;&nbsp;课程总时长:{{course.lessons}}课时/{{course.lessons==course.pub_lessons?'更新完成':('已更新'+course.pub_lessons+"课程")}}&nbsp;&nbsp;&nbsp;&nbsp;难度:{{course.course_level}}</p>
                <div v-if="course.get_course_discount_type">
                <div class="sale-time">
                  <p class="sale-type">{{course.get_course_discount_type}}</p>
                  <p class="expire">距离结束:仅剩 01天 04小时 33分 <span class="second">08</span></p>
                </div>
                <p class="course-price">
                  <span>活动价</span>
                  <span class="discount">¥{{course.get_course_price}}</span>
                  <span class="original">¥{{course.price}}</span>
                </p>
                </div>
                <div v-else>
                <div class="sale-time">
                  <p class="sale-type">价格: ¥{{course.price}}</p>
                </div>
                </div>
                <div class="buy">
                  <div class="buy-btn">
                    <button class="buy-now">立即购买</button>
                    <button class="free">免费试学</button>
                  </div>
                  <div @click="cartAddHander" class="add-cart"><img src="@/assets/cart-yellow.svg" alt="">加入购物车</div>
                </div>
              </div>
            </div>
            。。。。        
          </div>
          <Footer/>
        </div>
    </template>
    View Code

    实现课程详情页倒计时功能

    模型返回当前课程优惠的剩余时间戳

    courses/models.py中,Course模型新增计算剩余时间的方法,代码:

    from ckeditor_uploader.fields import RichTextUploadingField
    class Course(BaseModel):
        """
        专题课程
        """
        。。。
    
        def has_time(self):
            """计算活动的剩余时间"""
            now = datetime.now()
            try:
                course_prices = self.prices.get(start_time__lte=now, end_time__gte=now, is_delete=False, is_show=True)
                # 把 活动结束时间 - 当前时间 = 剩余时间
                return int( course_prices.end_time.timestamp() - now.timestamp() )
            except:
                print("---活动过期了----")
    
            return 0

     序列化器,新增返回字段

    class CourseDetailModelSerializer(serializers.ModelSerializer):
        """课程详情页的序列化器"""
        teacher = TeacherDetailModelSerializer()
        class Meta:
            model = Course
            # fields = ("id","name", "video", "course_img", "students","lessons","pub_lessons","price","teacher","course_level","brief")
            fields = ("id","name", "course_img", "students","lessons","pub_lessons","price","teacher","course_level","brief","get_course_price","get_course_discount_type","has_time")
    View Code
    python

    前端使用定时器setInterval完成倒计时功能

    <template>
        <div class="detail">
          <Header/>
          <div class="main">
            <div class="course-info">
              <div class="wrap-left">
                。。。。
                <div class="sale-time">
                  <p class="sale-type">{{course.get_course_discount_type}}</p>
                  <p class="expire">距离结束:仅剩 {{Math.floor(course.has_time/86400)}}天 {{Math.floor(course.has_time%86400/3600)}}小时 {{Math.floor(course.has_time%86400%3600/60)}}分 <span class="second">{{Math.floor(course.has_time%86400%3600%60)}}</span></p>
                </div>
                <p class="course-price">
                  <span>活动价</span>
                  <span class="discount">¥{{course.get_course_price}}</span>
                  <span class="original">¥{{course.price}}</span>
                </p>
                </div>
                <div v-else>
                <div class="sale-time">
                  <p class="sale-type">价格: ¥{{course.price}}</p>
                </div>
                </div>
                。。。。
    
    </template>
    <script>
    import Header from "./common/Header"
    import Footer from "./common/Footer"
    
    import {videoPlayer} from 'vue-video-player';
    
    export default {
        name: "Detail",
        。。。。
        created(){
          // 获取当前课程ID
          this.course_id = this.$route.query.id - 0;
          // 判断ID基本有效性
          let _this = this;
          if( isNaN(this.course_id) || this.course_id < 1 ){
            _this.$alert("无效的课程ID!","错误",{
              callback(){
                _this.$router.go(-1);
              }});
          }
          // 发送请求获取后端课程数据
          this.$axios.get(this.$settings.Host+`/courses/detail/${this.course_id}/`).then(response=>{
            this.course = response.data;
            // 修改视频中的封面图片
            this.playerOptions.poster = this.course.course_img;
            // 倒计时
            if(this.course.has_time > 1){
    
              let timer = setInterval(()=>{
                if( this.course.has_time > 1 ){
                  this.course.has_time-=1;
                }else{
                  clearInterval(timer);
                  location.reload();
                }
              },1000);
    
            }
    
          }).catch(error=>{
            console.log(error.response)
          });
        },
        。。。。
    }
    </script>
    View Code

    后端实现提供课程有效期的API

    模型代码:

    """课程有效期"""
    class CourseTime(BaseModel):
        """课程有效期表"""
        timer = models.IntegerField(verbose_name="购买周期",default=30,help_text="单位:天<br>建议按月书写,例如:1个月,则为30.")
        title = models.CharField(max_length=150, null=True, blank=True, verbose_name="购买周期的文本提示", default="1个月有效", help_text="要根据上面的购买周期,<br>声明对应的提示内容,<br>展示在购物车商品列表中")
        course = models.ForeignKey("Course", on_delete=models.CASCADE, related_name="prices", verbose_name="课程")
        price = models.DecimalField(max_digits=6, decimal_places=2, verbose_name="课程原价", default=0)
    
        class Meta:
            db_table = "ly_course_time"
            verbose_name = "课程有效期表"
            verbose_name_plural = "课程有效期表"
    
        def __str__(self):
            return "课程:%s,周期:%s,价格:%s" % (self.course, self.timer, self.price)
    View Code

    数据迁移

    python manage.py makemigrations
    python manage.py migrate

    把模型注册到xadmin中。

    from .models import CourseTime
    class CourseTimeModelAdmin(object):
        """课程与价格优惠关系模型管理类"""
        list_display = ["course","title","timer","price"]
    xadmin.site.register(CourseTime, CourseTimeModelAdmin)

    购物车视图中,返回购买课程的周期列表,cart/views.py,代码

    class CartAPIView(APIView):
        permission_classes = [IsAuthenticated]
        """购物车视图"""
        def get(self,request):
            """获取购物车商品课程列表"""
            # 获取当前用户ID
            # user_id = 1
            user_id = request.user.id
            # 通过用户ID获取购物车中的商品信息
            redis = get_redis_connection("cart")
            cart_goods_list = redis.hgetall("cart_%s" % user_id ) # 商品课程列表
            cart_goods_selects = redis.smembers("cart_selected_%s" % user_id)
            # redis里面的所有数据最终都是以bytes类型的字符串保存的
            # print( cart_goods_selects ) # 格式: {b'7', b'3', b'5'}
            # print( cart_goods_list ) # 格式: {b'7': b'-1', b'5': b'-1'}
            # 遍历购物车中的商品课程到数据库获取课程的价格, 标题, 图片
            data_list = []
            try:
                for course_id_bytes,expire_bytes in cart_goods_list.items():
                    course_id = int( course_id_bytes.decode() )
                    expire    = expire_bytes.decode()
                    course = Course.objects.get(pk=course_id)
    
                    # 获取购买的课程的周期价格列表
                    expires = course.coursetimes.all()
                    # 默认具有永久价格
                    expire_list = [{
                        "title": "永久有效",
                        "timer": -1,
                        "price": course.price
                    }]
                    for item in expires:
                        expire_list.append({
                            "title":item.title,
                            "timer":item.timer,
                            "price":item.price,
                        })
    
                    data_list.append({
                        "id": course_id,
                        "expire":expire,
                        "course_img": course.course_img.url,
                        "name": course.name,
                        "price": course.get_course_price(),
                        "is_select": course_id_bytes in cart_goods_selects,
                        "expire_list": expire_list,
                    })
            except:
                return Response(data_list,status=status.HTTP_500_INTERNAL_SERVER_ERROR)
            # print(data_list)
            # 返回查询结果
            return Response(data_list,status=status.HTTP_200_OK)
    View Code

    前端展示购物车中每一个商品课程的购买周期,代码:

    <template>
        。。。。
                <el-table-column label="有效期" width="216">
                  <template slot-scope="scope">
                    <el-select @change="ChangeExpire(scope.row)" v-model="scope.row.expire" placeholder="请选择">
                      <el-option v-for="item in scope.row.expire_list" :key="item.timer" :label="item.title" :value="item.timer"></el-option>
                    </el-select>
                  </template>
                </el-table-column>
                。。。。
        </div>
    </template>
    
    <script>
    import Header from "./common/Header"
    import Footer from "./common/Footer"
    export default {
        name: "Cart",
        data(){
          return{
            // 注释掉原来的有效周期测试数据
            // expire:3,
            // expire_list:[]
            courseData:[], // 购物车中的商品信息
            selection:[],  // 购物车中被勾选的商品信息
            total_price:0.00,
          }
        },
        。。。。
        created(){
          // 判断是否登录
          this.token = sessionStorage.token || localStorage.token;
          if( !this.token ){
            this.$confirm("对不起,您尚未登录!请登录",'提示').then(() => {
              this.$router.push("/login");
            }).catch(()=>{
              this.$router.go(-1);
            });
          }else{
            // 获取购物车商品数据
            this.$axios.get(this.$settings.Host+"/carts/course/",{
              headers:{
                // 注意下方的空格!!!
                "Authorization":"jwt " + this.token
              }
            }).then(response=>{
    
              this.courseData = response.data;
              // 更新在vuex里面的数据
              this.$store.state.cart.count = response.data.length;
    
              // 调整因为ajax数据请求导致勾选状态人没有出现的原因,使用定时器进行延时调用
              setTimeout(()=>{
                let expire_data = [];
                this.courseData.forEach(course=>{
                  course.expire_list.forEach(row=>{
                    expire_data[row.timer] = row.title;
                  });
                });
    
                // row 就是字典数据[json]
                this.courseData.forEach(row => {
                  // 设置商品课程的选中状态
                  if(row.is_select){
                    this.$refs.multipleTable.toggleRowSelection(row);
                  }
                  // 调整有效期选项中数值变成文本内容
                  row.expire = expire_data[row.expire];
                });
              },0)
    
            }).catch(error=>{
              let status = error.response.status;
              if( status == 401 ){
                this.token = null;
                sessionStorage.removeItem("token");
                localStorage.removeItem("token");
                let _this = this;
                this.$alert("您尚未登录或登录超时!请重新登录","警告",{
                  callback(){
                    _this.$router.push("/login");
                  }
                });
              }
            })
    
          }
        },
        
        。。。
    }
    </script>
    View Code

    当切换课程周期时,后端重新计算价格并返回

    模型中计算真实价格时,增加一个原价字段,通过原价字段,判断本次计算是计算周期还是计算永久有效。

    from ckeditor_uploader.fields import RichTextUploadingField
    class Course(BaseModel):
        """
        专题课程
        """
        。。。。
    
        def get_course_price(self,price=0):
            # 获取当前课程的真实价格
    
            self.price = price if price != 0 else self.price # 判断调用当前方法时,是否定义了价格
            
            。。。。

    视图中修改patch方法,在用户切换购买课程周期时,重新计算真实课程价格,代码:

    def patch(self,request):
            """更新购物城中的商品信息[切换课程有效期]"""
            # 获取当前登录的用户ID
            # user_id = 1
            user_id = request.user.id
    
            # 获取当前操作的课程ID
            course_id = request.data.get("course_id")
    
            # 获取新的有效期
            expire = request.data.get("expire")
    
            # 获取redis链接
            redis = get_redis_connection("cart")
    
            # 更新购物中商品课程的有效期
            redis.hset("cart_%s" % user_id,course_id, expire)
    
            # 根据新的课程有效期获取新的课程原价
            try:
                coursetime = CourseTime.objects.get(course=course_id, timer=expire)
                # 根据新的课程价格,计算真实课程价格
                price = coursetime.course.get_course_price(coursetime.price)
            except:
                # 这里给price设置一个默认值,当值-1,则前段不许要对价格进行调整
                course = Course.objects.get(pk=course_id)
                price = course.get_course_price()
    
    
            return Response({
                "price": price,
                "message": "修改购物车信息成功!"
            }, status=status.HTTP_200_OK)

    前端获取课程有效期列表

          // 更新课程的有效期
          ChangeExpire(course){
            // 获取课程ID和有效期
            let course_id = course.id;
            let expire    = course.expire;
    
            // 发送patch请求更新有效期
            this.$axios.patch(this.$settings.Host+"/carts/course/",{
              course_id,
              expire,  // 这里是简写,相当于 expire:expire,
            },{
              headers:{
                // 注意下方的空格!!!
                "Authorization":"jwt " + this.token
              },
            }).then(response=>{
              // 更新购买的商品课程的价格
              course.price = response.data.price;
              this.$message(response.data.message,"提示");
            });
          },
    View Code

    完成上面的步骤以后,切换购买周期时,价格就发生了变化,但是购物车页面刷新时,发现价格还原成"永久有效"的价格。所以我们需要在后端的购物车商品列表api接口中针对价格的购买周期进行判断。

    carts/views.py的CartAPIView

        def get(self,request):
            """获取购物车商品课程列表"""
            # 获取当前用户ID
            # user_id = 1
            user_id = request.user.id
            # 通过用户ID获取购物车中的商品信息
            redis = get_redis_connection("cart")
            cart_goods_list = redis.hgetall("cart_%s" % user_id ) # 商品课程列表
            cart_goods_selects = redis.smembers("cart_selected_%s" % user_id)
            # redis里面的所有数据最终都是以bytes类型的字符串保存的
            # print( cart_goods_selects ) # 格式: {b'7', b'3', b'5'}
            # print( cart_goods_list ) # 格式: {b'7': b'-1', b'5': b'-1'}
            # 遍历购物车中的商品课程到数据库获取课程的价格, 标题, 图片
            data_list = []
            try:
                for course_id_bytes,expire_bytes in cart_goods_list.items():
                    course_id = int( course_id_bytes.decode() )
                    expire    = expire_bytes.decode()
                    course = Course.objects.get(pk=course_id)
    
                    # 获取购买的课程的周期价格列表
                    expires = course.coursetimes.all()
                    # 默认具有永久价格
                    expire_list = [{
                        "title": "永久有效",
                        "timer": -1,
                        "price": course.price
                    }]
                    for item in expires:
                        expire_list.append({
                            "title":item.title,
                            "timer":item.timer,
                            "price":item.price,
                        })
    
                    try:
                        # 根据课程有效期传入课程原价
                        coursetime = CourseTime.objects.get(course=course_id, timer=expire)
                        # 根据新的课程价格,计算真实课程价格
                        price= coursetime.price
                    except:
                        price = 0
    
                    data_list.append({
                        "id": course_id,
                        "expire":expire,
                        "course_img": course.course_img.url,
                        "name": course.name,
                        "price": course.get_course_price(price),
                        "is_select": course_id_bytes in cart_goods_selects,
                        "expire_list": expire_list,
                    })
            except:
                return Response(data_list,status=status.HTTP_500_INTERNAL_SERVER_ERROR)
            # print(data_list)
            # 返回查询结果
            return Response(data_list,status=status.HTTP_200_OK)
    View Code

    最后,修复在用户切换购买周期时,前端需要重新计算购物车中所有商品的总价格。

    // 更新课程的有效期
          ChangeExpire(course){
            // 获取课程ID和有效期
            let course_id = course.id;
            let expire    = course.expire;
    
            // 发送patch请求更新有效期
            this.$axios.patch(this.$settings.Host+"/carts/course/",{
              course_id,
              expire,  // 这里是简写,相当于 expire:expire,
            },{
              headers:{
                // 注意下方的空格!!!
                "Authorization":"jwt " + this.token
              },
            }).then(response=>{
              // 更新购买的商品课程的价格
              course.price = response.data.price;
              // 重新计算购物车中的商品总价
              this.getTotalPrice();
              this.$message(response.data.message,"提示");
            });
          },
    View Code
  • 相关阅读:
    0528习题 11-15
    通过文档算学生的平均分
    给定两个列表,转换为 DataFrame 类型
    一千美元的故事(钱放入信封中)
    pandas 几个重要知识点
    python文件操作
    是否感染病毒
    安装 kreas 2.2.4 版本问题
    小技巧_01
    【Liunx】Linux 系统启动过程
  • 原文地址:https://www.cnblogs.com/yang950718/p/10883579.html
Copyright © 2020-2023  润新知