luffcc项目-09-购物车页面,前端购物车初始列表页、后端购物车实现、添加课程商品到购物车的API接口实现、vuex、前端提交课程到后端添加购物车数据、价格优惠策略

购物车页面

一、前端购物车初始列表页

1.购物车页面由两部分构成:

Cart.vue,代码:

<template>
    <div class="cart">
      <Vheader></Vheader>
      <div class="cart_info">
        <div class="cart_title">
          <span class="text">我的购物车</span>
          <span class="total">共4门课程</span>
        </div>
        <div class="cart_table">
          <div class="cart_head_row">
            <span class="doing_row"></span>
            <span class="course_row">课程</span>
            <span class="expire_row">有效期</span>
            <span class="price_row">单价</span>
            <span class="do_more">操作</span>
          </div>
          <div class="cart_course_list">
            <CartItem></CartItem>
            <CartItem></CartItem>
            <CartItem></CartItem>
            <CartItem></CartItem>
          </div>
          <div class="cart_footer_row">
            <span class="cart_select"><label> <el-checkbox v-model="checked"></el-checkbox><span>全选</span></label></span>
            <span class="cart_delete"><i class="el-icon-delete"></i> <span>删除</span></span>
            <span class="goto_pay">去结算</span>
            <span class="cart_total">总计:¥0.0</span>
          </div>
        </div>
      </div>
      <Footer></Footer>
    </div>
</template>

<script>
import Vheader from "./common/Vheader"
import Footer from "./common/Footer"
import CartItem from "./common/CartItem"
export default {
    name: "Cart",
    data(){
      return {
        checked: false,
      }
    },
    methods:{

    },
    components:{
      Vheader,
      Footer,
      CartItem,
    }
}
</script>

<style scoped>
.cart_info{
  width: 1200px;
  margin: 0 auto 50px;
}
.cart_title{
  margin: 25px 0;
}
.cart_title .text{
  font-size: 18px;
  color: #666;
}
.cart_title .total{
  font-size: 12px;
  color: #d0d0d0;
}
.cart_table{
  width: 1170px;
}
.cart_table .cart_head_row{
  background: #F7F7F7;
  width: 100%;
  height: 80px;
  line-height: 80px;
  padding-right: 30px;
}
.cart_table .cart_head_row::after{
  content: "";
  display: block;
  clear: both;
}
.cart_table .cart_head_row .doing_row,
.cart_table .cart_head_row .course_row,
.cart_table .cart_head_row .expire_row,
.cart_table .cart_head_row .price_row,
.cart_table .cart_head_row .do_more{
  padding-left: 10px;
  height: 80px;
  float: left;
}
.cart_table .cart_head_row .doing_row{
  width: 78px;
}
.cart_table .cart_head_row .course_row{
  width: 530px;
}
.cart_table .cart_head_row .expire_row{
  width: 188px;
}
.cart_table .cart_head_row .price_row{
  width: 162px;
}
.cart_table .cart_head_row .do_more{
  width: 162px;
}

.cart_footer_row{
  padding-left: 36px;
  background: #F7F7F7;
  width: 100%;
  height: 80px;
  line-height: 80px;
}
.cart_footer_row .cart_select span{
  margin-left: 14px;
  font-size: 18px;
  color: #666;
}
.cart_footer_row .cart_delete{
  margin-left: 58px;
}
.cart_delete .el-icon-delete{
  font-size: 18px;
}

.cart_delete span{
  margin-left: 15px;
  cursor: pointer;
  font-size: 18px;
  color: #666;
}
.cart_total{
  float: right;
  margin-right: 62px;
  font-size: 18px;
  color: #666;
}
.goto_pay{
  float: right;
  width: 159px;
  height: 80px;
  outline: none;
  border: none;
  background: #ffc210;
  font-size: 18px;
  color: #fff;
  text-align: center;
  cursor: pointer;
}
</style>




Common/Cartitem.vue,代码:

<template>
    <div class="cart_item">
      <div class="cart_column column_1">
        <el-checkbox class="my_el_checkbox" v-model="checked"></el-checkbox>
      </div>
      <div class="cart_column column_2">
        <img src="/static/img/course-cover.jpeg" alt="">
        <span><router-link to="/course/detail/1">爬虫从入门到进阶</router-link></span>
      </div>
      <div class="cart_column column_3">
        <el-select v-model="expire" size="mini" placeholder="请选择购买有效期" class="my_el_select">
          <el-option label="1个月有效" value="30" key="30"></el-option>
          <el-option label="2个月有效" value="60" key="60"></el-option>
          <el-option label="3个月有效" value="90" key="90"></el-option>
          <el-option label="永久有效" value="10000" key="10000"></el-option>
        </el-select>
      </div>
      <div class="cart_column column_4">¥499.0</div>
      <div class="cart_column column_4">删除</div>
    </div>
</template>

<script>
export default {
    name: "CartItem",
    data(){
      return {
        checked:false,
        expire: "1个月有效",
      }
    }
}
</script>

<style scoped>
  /*.cart_item{*/
  /*  height: 100px;*/
  /*}*/
.cart_item::after{
  content: "";
  display: block;
  clear: both;
}
.cart_column{
  float: left;
  height: 150px;
  display: flex;
  align-items: center;
}
.cart_item .column_1{
  width: 88px;
  position: relative;
}
.my_el_checkbox{
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  top: 0;
  margin: auto;
  width: 16px;
  height: 16px;
}
.cart_item .column_2 {
  /*padding: 67px 10px;*/
  width: 520px;
  /*height: 116px;*/
}
.cart_item .column_2 img{
  width: 175px;
  /*height: 115px;*/
  margin-right: 35px;
  /*vertical-align: middle;*/
}
.cart_item .column_3{
  width: 197px;
  position: relative;
  padding-left: 10px;
}
.my_el_select{
  width: 117px;
  height: 28px;
  position: absolute;
  top: 0;
  bottom: 0;
  margin: auto;
}
.cart_item .column_4{
  /*padding: 67px 10px;*/
  /*height: 116px;*/
  width: 142px;
  /*line-height: 116px;*/
}

</style>


2.前端路由:
...
import Cart from '@/components/Cart'

...
	{
      path: '/cart',

      component: Cart
    },

二、后端购物车实现

1.创建子应用 cart
E:\axiangmu\luffcc\lyapi\lyapi\apps>python ../../manage.py startapp cart
2.注册子应用cart
INSTALLED_APPS = [
    'ckeditor',  # 富文本编辑器
    'ckeditor_uploader',  # 富文本编辑器上传图片模块

    'home',
    'users',
    'course',
    'django_filters',
    'cart',
]

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

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

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

1.cart/views.py视图,代码:
from django.shortcuts import render

# Create your views here.
from rest_framework.viewsets import ViewSet
from django_redis import get_redis_connection
from course import models
from rest_framework.response import Response
from rest_framework import status

class AddCartView(ViewSet):

    def add(self, request):

        course_id = request.data.get('course_id')
        user_id = 1
        conn = get_redis_connection('cart')

        try:
            models.Course.objects.get(id=course_id)
        except:

            return Response({'msg': '课程不存在'}, status=status.HTTP_400_BAD_REQUEST)

        conn.sadd('cart_%s' % user_id, course_id)
        cart_length = conn.scard('cart_%s' % user_id)
        print('cart_length', cart_length)

        return Response({'msg': '添加成功', 'cart_length': cart_length})

2.提供访问路由

总路由,代码:

path(r'cart/',include('cart.urls')),

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

from django.urls import path,re_path
from . import views

urlpatterns = [
    path('add_cart/', views.AddCartView.as_view({'post':'add'}))

]

为了保证系统的日志记录可以跟进redis部分的,我们还可以在之前自定义异常处理中增加关于 redis的异常捕获
utils/execptions.py,代码:

from rest_framework.views import exception_handler

from django.db import DatabaseError
from redis import RedisError

from rest_framework.response import Response
from rest_framework import status

import logging
logger = logging.getLogger('django')


def custom_exception_handler(exc, context):
    """
    自定义异常处理
    :param exc: 异常类
    :param context: 抛出异常的上下文
    :return: Response响应对象
    """
    # 调用drf框架原生的异常处理方法
    response = exception_handler(exc, context)

    if response is None:
        view = context['view']  # 错误出现的那个函数或者方法
        if isinstance(exc, DatabaseError) or isinstance(exc, RedisError):
            # 数据库异常
            logger.error('[%s] %s' % (view, exc))
            response = Response({'message': '服务器内部错误'}, status=status.HTTP_507_INSUFFICIENT_STORAGE)

    return response

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

1.总Detail.vue
<template>
    <div class="detail">
      <Vheader/>
      <div class="main">
        <div class="course-info">
          <div class="wrap-left">
            <videoPlayer
               class="video-player vjs-custom-skin"
               ref="videoPlayer"
               :playsinline="true"
               :options="playerOptions"
               @play="onPlayerPlay($event)"
               @pause="onPlayerPause($event)">
            </videoPlayer>

          </div>
          <div class="wrap-right">
            <h3 class="course-name">{{ course_data.name }}</h3>
            <p class="data">{{course_data.students}}人在学&nbsp;&nbsp;&nbsp;&nbsp;课程总时长:{{course_data.lessons}}&nbsp;&nbsp;&nbsp;&nbsp;难度:{{course_data.level_name}}</p>
            <div class="sale-time">
              <p class="sale-type">{{course_data.discount_name}}</p>
              <p class="expire">距离结束:仅剩 {{course_data.left_time/60/60/24 | pInt}}天 {{course_data.left_time/60/60 % 24| pInt}}小时 {{course_data.left_time/60 % 60 | pInt}}分 <span class="second">{{course_data.left_time % 60 | pInt}}</span></p>
            </div>
            <p class="course-price">
              <span>活动价</span>
              <span class="discount">¥{{course_data.real_price}}</span>
              <span class="original">¥{{course_data.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="addCart"><img src="/static/img/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 class="course-brief" v-html="course_data.new_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_data.length}}章</p>
              </div>
              <div class="chapter-item" v-for="(chapter, chapterindex) in chapter_data">
                <p class="chapter-title"><img src="/static/img/1.png" alt="">第{{chapter.chapter}}章·{{chapter.name}}</p>
                <ul class="lesson-list">
                  <li class="lesson-item" v-for="(lesson, lesson_index) in chapter.coursesections">
                    <p class="name"><span class="index">{{chapter.chapter}}-{{lesson.lesson}}</span> 课程介绍-{{lesson.name}}<span v-show="lesson.free_trail" class="free">免费</span></p>
                    <p class="time">{{lesson.duration}} <img src="/static/img/chapter-player.svg"></p>
                    <button class="try" v-if="lesson.free_trail">立即试学</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="">
                   <div class="name">
                     <p class="teacher-name">{{course_data.teacher.name}}</p>
                     <p class="teacher-title">{{course_data.teacher.title}}</p>
                   </div>
                 </div>
                 <p class="narrative" >{{course_data.teacher.signature}}</p>
               </div>
             </div>
          </div>
        </div>
      </div>
      <Footer/>
    </div>
</template>

<script>
import Vheader from "./common/Vheader"
import Footer from "./common/Footer"
import {videoPlayer} from 'vue-video-player';


export default {
    name: "Detail",
    data(){
      return {
        tabIndex:1,
        course_id:0,
        token:'',

        course_data:{
          teacher:{}
        },
        chapter_data:{},
        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://www.lyapi.com:8001/media/video/777.mp4" //你的视频地址(必填)
          }],
          poster: "", //视频封面图
          width: document.documentElement.clientWidth, // 默认视频全屏时的最大宽度
          notSupportedMessage: '此视频暂无法播放,请稍后再试', //允许覆盖Video.js无法播放媒体源时显示的默认信息。
        }
      }

    },
    created(){
      this.get_course_id();
      this.get_course_data();
      this.get_chapter_data();
    },
    filters:{
      pInt(val){
        let a = parseInt(val);
        if (a < 10){
          a = `0${a}`;
        }
        return a
      }
    },

    methods: {
      addCart() {

        let token = localStorage.token || sessionStorage.token;

        if (token){
          this.$axios.post(`${this.$settings.Host}/users/verify/`,{
            token:token,
              }).then((res)=>{


              this.$axios.post(`${this.$settings.Host}/cart/add_cart/`,{
                  course_id:this.course_id,
                }).then((res)=>{
                  this.$message.success(res.data.msg);
                  this.$store.commit('add_cart', res.data.cart_length);
                  console.log(this.$store.state);
                })

              }).catch((error)=>{
                //console.log(error)

                this.token = false;
                this.$confirm('您还没登录?', '提示', {
                  confirmButtonText: '去登录',
                  cancelButtonText: '取消',
                  type: 'warning'
                }).then(() => {
                  this.$router.push('/user/login');
                })
                sessionStorage.removeItem('token');
                sessionStorage.removeItem('username');
                sessionStorage.removeItem('id');
                localStorage.removeItem('token');
                localStorage.removeItem('username');
                localStorage.removeItem('id');
              })
        } else {
          this.$confirm('您还没登录?', '提示', {
                  confirmButtonText: '去登录',
                  cancelButtonText: '取消',
                  type: 'warning'
                }).then(() => {
                  this.$router.push('/user/login');
                })
        }

      },


      get_course_id(){
        this.course_id = this.$route.params.id;
        // 可以判断course_id的合法性
      },

      get_course_data(){
        this.$axios.get(`${this.$settings.Host}/course/detail/${this.course_id}/`)
        .then((res)=>{
          //console.log(res.data);
          this.course_data = res.data;
          this.playerOptions.sources[0].src = res.data.course_video
          this.playerOptions.poster = res.data.course_img


          setInterval(()=>{
            this.course_data.left_time--;

          },1000)

        })
      },

      get_chapter_data(){
        this.$axios.get(`${this.$settings.Host}/course/chapter/`,{
          params:{
            course:this.course_id,
          }
        }).then((res)=>{
          //console.log(res.data);
          this.chapter_data = res.data
        })
      },


      onPlayerPlay(e){
        alert('开始播放');
      },
      onPlayerPause(e){
        alert('暂停播放');
      },
    },
    components:{
      Vheader,
      Footer,
      videoPlayer,
    }
}
</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>


settings.js

import th from "element-ui/src/locale/lang/th";
import fa from "element-ui/src/locale/lang/fa";

export default {
  Host:"http://www.lyapi.com:8001",// server address

  check_login(ths){
      let token = localStorage.token || sessionStorage.token;
      //console.log(this.token);
      console.log('>>>>>',token);
      //console.log('>>>>>',ths.$axios);

      if (token){
        ths.$axios.post(`${this.Host}/users/verify/`,{
          token:token,
            }).then((res)=>{

              console.log('ooooo', token);
              ths.token = token;

            }).catch((error)=>{
              //console.log(error)

              ths.token = false;
              sessionStorage.removeItem('token');
              sessionStorage.removeItem('username');
              sessionStorage.removeItem('id');
              localStorage.removeItem('token');
              localStorage.removeItem('username');
              localStorage.removeItem('id');
            })
      } else {
        ths.token = false
      }
  }

}

填入video路径:

2.前端展示商品课程的总数

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

安装vuex

npm install -S vuex

把vuex注册到vue中

在src目录下创建store目录,并在store目录下创建一个index.js文件,index.js文件代码:

import Vue from 'vue'

import Vuex from 'vuex'

Vue.use(Vuex);

export default new Vuex.Store({
  state: {
    cart_length: 0,

  },
  mutations: {
    add_cart (state, cart_length) {
      state.cart_length = cart_length;
    }

  }
})

把上面index.js中创建的store对象注册到main.js的vue中

...
import store from './store';  //引入
...

...
new Vue({
  el: '#app',
  router,
  store,  // 挂载
  components: { App },
  template: '<App/>'
})

购物车显示课程个数,Vheader.vue

				...
				<router-link to="/cart/">
                  <b>{{$store.state.cart_length}}</b>
                  <img src="@/assets/shopcart.png" alt="">
                  <span>购物车 </span>
                </router-link>
                ...

*** vuex保存数据是在内存中的,刷新页面的时候会导致vuex中的所有属性重新加载,导致保存在vuex中的数据丢失

App.vue
export default {
  name: 'App',
  created() {
    window.addEventListener('beforeunload', ()=>{
      console.log('页面要刷新啦!,赶紧保存数据!');
      sessionStorage.setItem('cart_length', this.$store.state.cart_length);
    })
  }
}

Vheader.vue:加载的时候,重新去sessionStorage中获取数据,并再次保存到vuex中
	created(){
        this.get_nav_data();
        // console.log('xxxxxxxxxxxxxxx')
        // this.$settings.check_login(this)
        this.$settings.check_login(this);

        if (this.$store.state.cart_length === 0){
          let cart_length = sessionStorage.getItem('cart_length');
          this.$store.commit('add_cart',cart_length);
          console.log(this.$store.state);
        }

      },
3.前端请求并显示课程信息

Cart.vue

<template>
    <div class="cart">
      <Vheader></Vheader>
      <div class="cart_info">
        <div class="cart_title">
          <span class="text">我的购物车</span>
          <span class="total">共{{$store.state.total}}门课程</span>
        </div>
        <div class="cart_table">
          <div class="cart_head_row">
            <span class="doing_row"></span>
            <span class="course_row">课程</span>
            <span class="expire_row">有效期</span>
            <span class="price_row">单价</span>
            <span class="do_more">操作</span>
          </div>
          <div class="cart_course_list">
            <CartItem v-for="cart in cart_list" :cart="cart" :key="cart.id"></CartItem>

          </div>
          <div class="cart_footer_row">
            <span class="cart_select"><label> <el-checkbox v-model="checked"></el-checkbox><span>全选</span></label></span>
            <span class="cart_delete"><i class="el-icon-delete"></i> <span>删除</span></span>
            <span class="goto_pay">去结算</span>
            <span class="cart_total">总计:¥0.0</span>
          </div>
        </div>
      </div>
      <Footer></Footer>
    </div>
</template>

<script>
import Vheader from "./common/Vheader"
import Footer from "./common/Footer"
import CartItem from "./common/CartItem"
export default {
    name: "Cart",
    data(){
      return {
        cart_list: [],
        checked: false,
        user_token: '',
      }
    },
    created() {
      this.user_token = this.check_user_login();
      this.get_cart();
    },
    methods:{
      check_user_login(){
        let user_token = localStorage.user_token || sessionStorage.user_token;
        if (!user_token){
          this.$confirm("对不起,您尚未登录!请登录后继续操作!", "警告").then(()=>{
            this.$router.push("/user/login");
          });
        }
        return user_token;
      },
      get_cart(){
        this.$axios.get(`${this.$settings.Host}/cart/course/get`,{
          headers:{
            'Authorization': 'jwt' + this.user_token,
          }
        }).then((response)=>{
          this.cart_list = response.data;
        }).catch((error)=>{
          console.log(error.response)
        })
      }

    },
    components:{
      Vheader,
      Footer,
      CartItem,
    }
}
</script>

<style scoped>
.cart_info{
  width: 1200px;
  margin: 0 auto 50px;
}
.cart_title{
  margin: 25px 0;
}
.cart_title .text{
  font-size: 18px;
  color: #666;
}
.cart_title .total{
  font-size: 12px;
  color: #d0d0d0;
}
.cart_table{
  width: 1170px;
}
.cart_table .cart_head_row{
  background: #F7F7F7;
  width: 100%;
  height: 80px;
  line-height: 80px;
  padding-right: 30px;
}
.cart_table .cart_head_row::after{
  content: "";
  display: block;
  clear: both;
}
.cart_table .cart_head_row .doing_row,
.cart_table .cart_head_row .course_row,
.cart_table .cart_head_row .expire_row,
.cart_table .cart_head_row .price_row,
.cart_table .cart_head_row .do_more{
  padding-left: 10px;
  height: 80px;
  float: left;
}
.cart_table .cart_head_row .doing_row{
  width: 78px;
}
.cart_table .cart_head_row .course_row{
  width: 530px;
}
.cart_table .cart_head_row .expire_row{
  width: 188px;
}
.cart_table .cart_head_row .price_row{
  width: 162px;
}
.cart_table .cart_head_row .do_more{
  width: 162px;
}

.cart_footer_row{
  padding-left: 36px;
  background: #F7F7F7;
  width: 100%;
  height: 80px;
  line-height: 80px;
}
.cart_footer_row .cart_select span{
  margin-left: 14px;
  font-size: 18px;
  color: #666;
}
.cart_footer_row .cart_delete{
  margin-left: 58px;
}
.cart_delete .el-icon-delete{
  font-size: 18px;
}

.cart_delete span{
  margin-left: 15px;
  cursor: pointer;
  font-size: 18px;
  color: #666;
}
.cart_total{
  float: right;
  margin-right: 62px;
  font-size: 18px;
  color: #666;
}
.goto_pay{
  float: right;
  width: 159px;
  height: 80px;
  outline: none;
  border: none;
  background: #ffc210;
  font-size: 18px;
  color: #fff;
  text-align: center;
  cursor: pointer;
}
</style>


CartItem.vue

<template>
    <div class="cart_item">
      <div class="cart_column column_1">
        <el-checkbox class="my_el_checkbox" v-model="checked"></el-checkbox>
      </div>
      <div class="cart_column column_2">
        <img src="/static/img/course-cover.jpeg" alt="">
        <span><router-link to="/course/detail/1">{{cart.name}}</router-link></span>
      </div>
      <div class="cart_column column_3">
        <el-select v-model="expire" size="mini" placeholder="请选择购买有效期" class="my_el_select">
          <el-option label="1个月有效" value="30" key="30"></el-option>
          <el-option label="2个月有效" value="60" key="60"></el-option>
          <el-option label="3个月有效" value="90" key="90"></el-option>
          <el-option label="永久有效" value="10000" key="10000"></el-option>
        </el-select>
      </div>
      <div class="cart_column column_4">¥{{cart.price.toFixed(2)}}</div>
      <div class="cart_column column_4">删除</div>
    </div>
</template>

<script>
export default {
    name: "CartItem",
  props: ['cart'],
    data(){
      return {
        checked:false,
        expire: "1个月有效",
      }
    }
}
</script>

<style scoped>
  /*.cart_item{*/
  /*  height: 100px;*/
  /*}*/
.cart_item::after{
  content: "";
  display: block;
  clear: both;
}
.cart_column{
  float: left;
  height: 150px;
  display: flex;
  align-items: center;
}
.cart_item .column_1{
  width: 88px;
  position: relative;
}
.my_el_checkbox{
  position: absolute;
  left: 0;
  right: 0;
  bottom: 0;
  top: 0;
  margin: auto;
  width: 16px;
  height: 16px;
}
.cart_item .column_2 {
  /*padding: 67px 10px;*/
  width: 520px;
  /*height: 116px;*/
}
.cart_item .column_2 img{
  width: 175px;
  /*height: 115px;*/
  margin-right: 35px;
  /*vertical-align: middle;*/
}
.cart_item .column_3{
  width: 197px;
  position: relative;
  padding-left: 10px;
}
.my_el_select{
  width: 117px;
  height: 28px;
  position: absolute;
  top: 0;
  bottom: 0;
  margin: auto;
}
.cart_item .column_4{
  /*padding: 67px 10px;*/
  /*height: 116px;*/
  width: 142px;
  /*line-height: 116px;*/
}

</style>

cart/views.py

from django.shortcuts import render

# Create your views here.
from rest_framework.viewsets import ViewSet
from django_redis import get_redis_connection
from course import models
from rest_framework.response import Response
from rest_framework import status
from lyapi.settings import contains

import logging
logger = logging.getLogger('django')

class AddCartView(ViewSet):

    def add(self, request):

        course_id = request.data.get('course_id')
        user_id = 1

        expire = 0  # 表示永久有效
        conn = get_redis_connection('cart')

        try:
            models.Course.objects.get(id=course_id)
        except:

            return Response({'msg': '课程不存在'}, status=status.HTTP_400_BAD_REQUEST)

        pipe = conn.pipeline()
        pipe.multi()

        # 批量操作
        pipe.hset('cart_%s' % user_id, course_id, expire)

        pipe.execute()

        # conn.sadd('cart_%s' % user_id, course_id)
        # cart_length = conn.scard('cart_%s' % user_id)
        cart_length = conn.hlen('cart_%s' % user_id)
        print('cart_length', cart_length)

        return Response({'msg': '添加成功', 'cart_length': cart_length})


    def cart_list(self, request):

        user_id = 1
        conn = get_redis_connection('cart')
        ret = conn.hgetall('cart_%s' % user_id)
        cart_data_list = []
        print(ret)
        try:
            for cid, eid in ret.items():
                course_id = cid.decode()
                expire_id = eid.decode()

                course_obj = models.Course.objects.get(id=course_id)

                cart_data_list.append({
                    'name': course_obj.name,
                    'course_img': contains.SERVER_ADDR + course_obj.course_img.url,
                    'price': course_obj.price,
                    'expire_id': expire_id,

                })
        except Exception:
            logger.error('获取购物车数据失败')
            return Response({'msg': '后台数据库出问题了,请联系管理员'}, status=status.HTTP_507_INSUFFICIENT_STORAGE)

        print(cart_data_list)

        return Response({'msg': 'xxx', 'cart_data_list': cart_data_list})

五、价格优惠策略

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


积分抵扣     总价-(积分计算后换算价格) ->> 积分换算比率
优惠券       总价-优惠券价格         -->> 优惠券

模型代码:
course/models.py



"""价格相关的模型"""
class CourseDiscountType(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_course_discount_type"
        verbose_name = "课程优惠类型"
        verbose_name_plural = "课程优惠类型"

    def __str__(self):
        return "%s" % (self.name)


class CourseDiscount(BaseModel):
    """课程优惠模型"""
    discount_type = models.ForeignKey("CourseDiscountType", on_delete=models.CASCADE, related_name='coursediscounts', verbose_name="优惠类型")
    condition = models.IntegerField(blank=True, default=0, verbose_name="满足优惠的价格条件",help_text="设置参与优惠的价格门槛,表示商品必须在xx价格以上的时候才参与优惠活动,<br>如果不填,则不设置门槛") #因为有的课程不足100,你减免100,还亏钱了
    sale = models.TextField(verbose_name="优惠公式",blank=True,null=True, help_text="""
    不填表示免费;<br>
    *号开头表示折扣价,例如*0.82表示八二折;<br>
    -号开头则表示减免,例如-20表示原价-20;<br>
    如果需要表示满减,则需要使用 原价-优惠价格,例如表示课程价格大于100,优惠10;大于200,优惠25,格式如下:<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-25<br>
    """)

    class Meta:
        db_table = "ly_course_discount"
        verbose_name = "价格优惠策略"
        verbose_name_plural = "价格优惠策略"

    def __str__(self):
        return "价格优惠:%s,优惠条件:%s,优惠值:%s" % (self.discount_type.name, self.condition, self.sale)

class Activity(BaseModel):
    """优惠活动"""
    name = models.CharField(max_length=150, verbose_name="活动名称")
    start_time = models.DateTimeField(verbose_name="优惠策略的开始时间")
    end_time = models.DateTimeField(verbose_name="优惠策略的结束时间")
    remark = models.CharField(max_length=250, blank=True, null=True, verbose_name="备注信息")

    class Meta:
        db_table = "ly_activity"
        verbose_name="商品活动"
        verbose_name_plural="商品活动"

    def __str__(self):
        return self.name

class CoursePriceDiscount(BaseModel):
    """课程与优惠策略的关系表"""
    course = models.ForeignKey("Course",on_delete=models.CASCADE, related_name="activeprices",verbose_name="课程")
    active = models.ForeignKey("Activity",on_delete=models.DO_NOTHING, related_name="activecourses",verbose_name="活动")
    discount = models.ForeignKey("CourseDiscount",on_delete=models.CASCADE,related_name="discountcourse",verbose_name="优惠折扣")

    class Meta:
        db_table = "ly_course_price_dicount"
        verbose_name="课程与优惠策略的关系表"
        verbose_name_plural="课程与优惠策略的关系表"

    def __str__(self):
        return "课程:%s,优惠活动: %s,开始时间:%s,结束时间:%s" % (self.course.name, self.active.name, self.active.start_time,self.active.end_time)

执行数据迁移

python manage.py makemigrations
python manage.py migrate

在xadmin中注册模型管理器,courses/adminx.py代码:

from .models import CourseDiscountType
class CourseExpireModelAdmin(object):
    """课程与有效期模型管理类"""
    pass
xadmin.site.register(CourseDiscountType, CourseExpireModelAdmin)

from .models import CourseDiscount
class PriceDiscountTypeModelAdmin(object):
    """价格优惠类型"""
    pass
xadmin.site.register(CourseDiscount, PriceDiscountTypeModelAdmin)


from .models import Activity
class PriceDiscountModelAdmin(object):
    """价格优惠公式"""
    pass
xadmin.site.register(Activity, PriceDiscountModelAdmin)


from .models import CoursePriceDiscount
class CoursePriceDiscountModelAdmin(object):
    """商品优惠和活动的关系"""
    pass
xadmin.site.register(CoursePriceDiscount, CoursePriceDiscountModelAdmin)
2.添加测试数据
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值