fifteen_手机短信认证登录_课程列表页

 

2. 用户的注册认证

# 前端显示注册页面并调整首页头部和登陆页面的注册按钮的链接。
# 注册页面Register,主要是通过登录页面进行改成而成.
<template>
    <div class="box">
        <img src="https://www.luffycity.com/static/img/Loginbg.3377d0c.jpg" alt="">
        <div class="register">
            <div class="register_box">
        <div class="register-title">注册路飞学城</div>
                <div class="inp">
                    <input v-model = "mobile" type="text" placeholder="手机号码" class="user">
                    <div id="geetest"></div>
                    <input v-model = "sms" type="text" placeholder="输入验证码" class="user">
                    <button class="register_btn" >注册</button>
                    <p class="go_login" >已有账号 <router-link to="/login">直接登录</router-link></p>
                </div>
            </div>
        </div>
    </div>
</template>

<script>
export default {
  name: 'Register',
  data(){
    return {
        sms:"",
        mobile:"",
        validateResult:false,
    }
  },
  created(){
  },
  methods:{},

};
</script>

<style scoped>
.box{
    width: 100%;
  height: 100%;
    position: relative;
  overflow: hidden;
}
.box img{
    width: 100%;
  min-height: 100%;
}
.box .register {
    position: absolute;
    width: 500px;
    height: 400px;
    top: 0;
    left: 0;
  margin: auto;
  right: 0;
  bottom: 0;
  top: -338px;
}
.register .register-title{
    width: 100%;
    font-size: 24px;
    text-align: center;
    padding-top: 30px;
    padding-bottom: 30px;
    color: #4a4a4a;
    letter-spacing: .39px;
}
.register-title img{
    width: 190px;
    height: auto;
}
.register-title p{
    font-family: PingFangSC-Regular;
    font-size: 18px;
    color: #fff;
    letter-spacing: .29px;
    padding-top: 10px;
    padding-bottom: 50px;
}
.register_box{
    width: 400px;
    height: auto;
    background: #fff;
    box-shadow: 0 2px 4px 0 rgba(0,0,0,.5);
    border-radius: 4px;
    margin: 0 auto;
    padding-bottom: 40px;
}
.register_box .title{
    font-size: 20px;
    color: #9b9b9b;
    letter-spacing: .32px;
    border-bottom: 1px solid #e6e6e6;
     display: flex;
        justify-content: space-around;
        padding: 50px 60px 0 60px;
        margin-bottom: 20px;
        cursor: pointer;
}
.register_box .title span:nth-of-type(1){
    color: #4a4a4a;
        border-bottom: 2px solid #84cc39;
}

.inp{
    width: 350px;
    margin: 0 auto;
}
.inp input{
    border: 0;
    outline: 0;
    width: 100%;
    height: 45px;
    border-radius: 4px;
    border: 1px solid #d9d9d9;
    text-indent: 20px;
    font-size: 14px;
    background: #fff !important;
}
.inp input.user{
    margin-bottom: 16px;
}
.inp .rember{
     display: flex;
    justify-content: space-between;
    align-items: center;
    position: relative;
    margin-top: 10px;
}
.inp .rember p:first-of-type{
    font-size: 12px;
    color: #4a4a4a;
    letter-spacing: .19px;
    margin-left: 22px;
    display: -ms-flexbox;
    display: flex;
    -ms-flex-align: center;
    align-items: center;
    /*position: relative;*/
}
.inp .rember p:nth-of-type(2){
    font-size: 14px;
    color: #9b9b9b;
    letter-spacing: .19px;
    cursor: pointer;
}

.inp .rember input{
    outline: 0;
    width: 30px;
    height: 45px;
    border-radius: 4px;
    border: 1px solid #d9d9d9;
    text-indent: 20px;
    font-size: 14px;
    background: #fff !important;
}

.inp .rember p span{
    display: inline-block;
  font-size: 12px;
  width: 100px;
  /*position: absolute;*/
/*left: 20px;*/

}
#geetest{
    margin-top: 20px;
}
.register_btn{
     width: 100%;
    height: 45px;
    background: #84cc39;
    border-radius: 5px;
    font-size: 16px;
    color: #fff;
    letter-spacing: .26px;
    margin-top: 30px;
}
.inp .go_login{
    text-align: center;
    font-size: 14px;
    color: #9b9b9b;
    letter-spacing: .26px;
    padding-top: 20px;
}
.inp .go_login span{
    color: #84cc39;
    cursor: pointer;
}
</style>

 

# 前端注册路由:
import Register from "../components/Register"

// 配置路由列表
export default new Router({
  mode:"history",
  routes:[
    // 路由列表
    ...
    {
      name:"Register",
      path: "/register",
      component:Register,
    }
  ]
})

 

# 修改首页头部的连接:
# Header.vue
<span class="header-register"><router-link to="/register">注册</router-link></span>
#Login.vue
<p class="go_login" >没有账号 <router-link to="/register">立即注册</router-link></p>

 

注册功能的实现

接下来,我们把注册过程中一些注册信息(例如:短信验证码)和session缓存到redis数据库中。

# 安装django-redis。
pip install django-redis

# 在settings.py配置中添加一下代码:
# 设置redis缓存
CACHES = {
    # 默认缓存
    "default": {
        "BACKEND": "django_redis.cache.RedisCache",
        # 项目上线时,需要调整这里的路径
        "LOCATION": "redis://127.0.0.1:6379/0",

        "OPTIONS": {
            "CLIENT_CLASS": "django_redis.client.DefaultClient",
        }
    },
    # 提供给xadmin或者admin的session存储
    "session": {
        "BACKEND": "django_redis.cache.RedisCache",
        "LOCATION": "redis://127.0.0.1:6379/1",
        "OPTIONS": {
            "CLIENT_CLASS": "django_redis.client.DefaultClient",
        }
    },
    # 提供存储短信验证码
    "sms_code":{
        "BACKEND": "django_redis.cache.RedisCache",
        "LOCATION": "redis://127.0.0.1:6379/2",
        "OPTIONS": {
            "CLIENT_CLASS": "django_redis.client.DefaultClient",
        }
    }
}

# 设置xadmin用户登录时,登录信息session保存到redis
SESSION_ENGINE = "django.contrib.sessions.backends.cache"
SESSION_CACHE_ALIAS = "session"

 

关于django-redis 的使用,说明文档可见http://django-redis-chs.readthedocs.io/zh_CN/latest/

django-redis提供了get_redis_connection的方法,通过调用get_redis_connection方法传递redis的配置名称可获取到redis的连接对象,通过redis连接对象可以执行redis命令

https://redis-py.readthedocs.io/en/latest/

# 使用范例:
from django_redis import get_redis_connection
// 链接redis数据库
redis_conn = get_redis_connection("default")

 

使用云通讯发送短信

在登录后的平台上面获取一下信息:

ACCOUNT SID:8aaf0708697b6beb01699f4442911776
AUTH TOKEN : b4dea244f43a4e0f90e557f0a99c70fa
AppID(默认):8aaf0708697b6beb01699f4442e3177c
Rest URL(生产): app.cloopen.com:8883         [项目上线时使用真实短信发送服务器]
Rest URL(开发): sandboxapp.cloopen.com:8883 [项目开发时使用沙箱短信发送服务器]

找到sdkdemo进行下载

 

 

 把云通讯的sdk保存到libs目录下, 并修改里面的基本配置信息。

# lib下的文件里的sms
# -*- coding:utf-8 -*-

from .CCPRestSDK import REST
from django.conf import settings
# 说明:主账号,登陆云通讯网站后,可在"控制台-应用"中看到开发者主账号ACCOUNT SID
_accountSid = settings.SMS_ACCOUNTSID

# 说明:主账号Token,登陆云通讯网站后,可在控制台-应用中看到开发者主账号AUTH TOKEN
_accountToken = settings.SMS_ACCOUNTTOKEN
# 6dd01b2b60104b3dbc88b2b74158bac6
# 请使用管理控制台首页的APPID或自己创建应用的APPID
_appId = settings.SMS_APPID
# 8a216da863f8e6c20164139688400c21
# 说明:请求地址,生产环境配置成app.cloopen.com
_serverIP = settings.SMS_SERVERIP

# 说明:请求端口 ,生产环境为8883
_serverPort = "8883"

# 说明:REST API版本号保持不变
_softVersion = '2013-12-26'

# 云通讯官方提供的发送短信代码实例
# # 发送模板短信
# # @param to 手机号码
# # @param datas 内容数据 格式为数组 例如:{'12','34'},如不需替换请填 ''
# # @param $tempId 模板Id
#
# def sendTemplateSMS(to, datas, tempId):
#     # 初始化REST SDK
#     rest = REST(serverIP, serverPort, softVersion)
#     rest.setAccount(accountSid, accountToken)
#     rest.setAppId(appId)
#
#     result = rest.sendTemplateSMS(to, datas, tempId)
#     for k, v in result.iteritems():
#
#         if k == 'templateSMS':
#             for k, s in v.iteritems():
#                 print '%s:%s' % (k, s)
#         else:
#             print '%s:%s' % (k, v)


class CCP(object):
    """发送短信的辅助类"""

    def __new__(cls, *args, **kwargs):
        # 判断是否存在类属性_instance,_instance是类CCP的唯一对象,即单例
        if not hasattr(CCP, "_instance"):
            cls._instance = super(CCP, cls).__new__(cls, *args, **kwargs)
            cls._instance.rest = REST(_serverIP, _serverPort, _softVersion)
            cls._instance.rest.setAccount(_accountSid, _accountToken)
            cls._instance.rest.setAppId(_appId)
        return cls._instance

    def send_template_sms(self, to, datas, temp_id):
        """发送模板短信"""
        # @param to 手机号码
        # @param datas 内容数据 格式为数组 例如:{'12','34'},如不需替换请填 ''
        # @param temp_id 模板Id
        result = self.rest.sendTemplateSMS(to, datas, temp_id)
        print(result)
        # 如果云通讯发送短信成功,返回的字典数据result中statuCode字段的值为"000000"
        if result.get("statusCode") == "000000":
            # 返回0 表示发送短信成功
            return 0
        else:
            # 返回-1 表示发送失败
            return -1


if __name__ == '__main__':
    ccp = CCP()
    # 注意: 测试的短信模板编号为1[以后申请了企业账号以后可以有更多的模板]
    # 参数1: 客户端手机号码,测试时只能发给测试号码
    # 参数2: 短信模块中的数据
    #        短信验证码
    #        短信验证码有效期提示
    # 参数3: 短信模板的id,开发测试时,只能使用1
    result = ccp.send_template_sms('13928835901', ['1234',5], 1)
    print(result)

 

# 再去dev里面配置对应的值
# 短信配置
# 主账号
SMS_ACCOUNTSID = '8a216da86ab05e69016ab0654874000e'

# 主账号Token
SMS_ACCOUNTTOKEN = '9f1c1***5d882'

# 创建应用的APPID
SMS_APPID = '8a216da86ab05e69016ab06548ea0014'

# 说明:请求地址,生产环境配置成app.cloopen.com
SMS_SERVERIP = 'sandboxapp.cloopen.com'

 

# 后端视图函数中生成短信验证码
from luffy.libs.yuntongxun.sms import CCP
from django_redis import get_redis_connection
class SMSAPIView(APIView):
    # url: users/sms/(?P<mobile>1[3-9]\d{9})
    def get(self,request,mobile):
        ccp = CCP()
        sms_code = "%04d" % random.randint(1,9999)
        result = ccp.send_template_sms(mobile,[sms_code, 5],1)

        if not result:
            """发送成功"""
            redis = get_redis_connection("sms_code")
            redis.setex("%s_sms_code" % mobile, 5*60, sms_code)

        return Response({"result":result})

# urls.py代码
re_path(r'sms/(?P<mobile>1[3-9]\d{9})/', views.SMSAPIView.as_view() ),

 

# 前端请求发送短信
# 调整前端的页面,添加一个发送短信功能
# html
          <div class="sms-box">
            <input v-model = "sms" type="text" placeholder="输入验证码" class="user">
            <div class="sms-btn" @click="smsHandle">点击发送短信</div>
          </div>

# css 代码
.sms-box{
  position: relative;
}
.sms-btn{
    font-size: 14px;
    color: #ffc210;
    letter-spacing: .26px;
    position: absolute;
    right: 16px;
    top: 10px;
    cursor: pointer;
    overflow: hidden;
    background: #fff;
    border-left: 1px solid #484848;
    padding-left: 16px;
    padding-bottom: 4px;
}

# script data里面的methods中代码:
methods:{
    // 发送短信
    smsHandle() {
      // 判断是否填写了手机
      if( !/^\d{11}$/.test(this.mobile) ){
        this.$alert('手机号码格式有误!', '警告');
        return false;
      }

      this.$axios.get(this.$settings.Host+`/users/sms/${this.mobile}/`).then(response=>{
        let data = response.data
        if( data.result == '-1' ){
          this.$alert("发送短信失败!","错误");
        }else{
          this.$alert("发送短信成功了!","成功");
        }
      }).catch(error=>{
          console.log(error.response)
      })

    },
    // 提交注册信息
    ....

 

实现倒计时显示

# 前端
<template> .... <div class="sms-box"> <input v-model = "sms" type="text" placeholder="输入验证码" class="user"> <div class="sms-btn" @click="smsHandle">{{sms_text}}</div> </div> .... </template> <script> export default { name: 'Register', data(){ return { sms:"", mobile:"", password:"", password2:"", validateResult:false, is_send:false, // 是否已经发送短信的状态 send_intervel:60, // 发送短信的间隔 sms_text:"点击发送短信", // 发送短信的提示 } }, methods:{ // 发送短信 smsHandle() { // 判断是否填写了手机 if( !/^\d{11}$/.test(this.mobile) ){ this.$alert('手机号码格式有误!', '警告'); return false; } // 判断是否在60s内有发送过短信,如果有则,不能点击发送 if(this.is_send){ this.$alert('60s内不能频繁发送短信!', '警告'); return false; } let _this = this; _this.$axios.get(_this.$settings.Host+`/users/sms/${_this.mobile}/`).then(response=>{ let data = response.data; if( data.result == '-1' ){ _this.$alert("发送短信失败!","错误"); }else{ _this.is_send = true; _this.$alert("发送短信成功了!","成功",{ callback(){ let num = _this.send_intervel let timer = setInterval(()=>{ if(num<1){ clearInterval(timer); _this.sms_text = "点击发送短信"; _this.is_send = false; }else{ num--; _this.sms_text = num+"后可继续点击发送"; } },1000) } }); } }).catch(error=>{ console.log(error.response) }) }, // 提交注册信息 registerHander(){ .... } }, }; </script>

 

# 后端视图函数判断(因为有些用户可能会马上刷新再访问)
class SMSAPIView(APIView):
    # url: users/sms/(?P<mobile>1[3-9]\d{9})
    def get(self,request,mobile):
        redis = get_redis_connection("sms_code")
        # 获取短信发送间隔
        try:
            interval = redis.get("%s_interval" % mobile)
            if interval:
                print(interval)
                return Response({"result":"-1"})
        except:
            pass

        ccp = CCP()
        sms_code = "%04d" % random.randint(1,9999)
        result = ccp.send_template_sms(mobile,[sms_code, 5],1)

        if not result:
            """发送成功"""

            redis.setex("%s_sms_code" % mobile, 5*60, sms_code)
            # 这里的值不重要,重要的是这个变量是否在redis被查找到
            redis.setex("%s_interval" % mobile, 60, 1)

        return Response({"result":result})

 

后端保存用户注册信息

创建序列化器对象[暂时不涉及到手机验证码功能]

from rest_framework import serializers
from .models import User
import re
from django_redis import get_redis_connection
class UserModelSerializer(serializers.ModelSerializer):
    sms_code = serializers.CharField(write_only=True, max_length=4,min_length=4,required=True,help_text="短信验证码")
    password2 = serializers.CharField(write_only=True,help_text="确认密码")
    token = serializers.CharField(read_only=True,help_text="jwt token值")
    class Meta:
        model = User
        fields = ["mobile","id","token","password","password2","username","sms_code"]
        extra_kwargs = {
            "id":{"read_only":True},
            "username":{"read_only":True},
            "password":{"write_only":True},
            "mobile":{"write_only":True}
        }


    def validate_mobile(self, mobile):
        # 验证格式
        result = re.match('^1[3-9]\d{9}$', mobile)
        if not result:
            raise serializers.ValidationError("手机号码格式有误!")

        # 验证唯一性
        try:
            user = User.objects.get(mobile=mobile)
            if user:
                raise serializers.ValidationError("当前手机号码已经被注册!")

        except User.DoesNotExist:
            pass

        return mobile


    def validate(self, attrs):

        # 判断密码长度
        password = attrs.get("password")
        if not re.match('^.{6,16}$', password):
            raise serializers.ValidationError("密码长度必须在6-16位之间!")

        # 判断密码和确认密码是否一致
        password2 = attrs.get("password2")
        if password != password2:
            raise serializers.ValidationError("密码和确认密码不一致!")

        # 验证短信验证码
        mobile = attrs.get("mobile")
        redis = get_redis_connection("sms_code")
        try:
            real_sms_code = redis.get("%s_sms_code" % mobile).decode()
        except:
            raise serializers.ValidationError("验证码不存在,或已经过期!")

        if real_sms_code != attrs.get("sms_code"):
            raise serializers.ValidationError("验证码不存在,或错误!")

        # 删除本次使用的验证码
        try:
            redis.delete("%s_sms_code" % mobile)
        except:
            pass

        return attrs

    def create(self, validated_data):
        """保存用户"""
        mobile = validated_data.get("mobile")
        password = validated_data.get("password")

        try:
            user = User.objects.create(
                mobile=mobile,
                username=mobile,
                password=password,
            )

            # 密码加密
            user.set_password(user.password)
            user.save()

        except:
            raise serializers.ValidationError("注册用户失败!")

        # 生成一个jwt
        from rest_framework_jwt.settings import api_settings

        jwt_payload_handler = api_settings.JWT_PAYLOAD_HANDLER
        jwt_encode_handler = api_settings.JWT_ENCODE_HANDLER

        payload = jwt_payload_handler(user)
        user.token = jwt_encode_handler(payload)

        return user

 

# 视图代码
from .serializers import UserModelSerializer
from rest_framework.generics import CreateAPIView
from .models import User
class UserAPIView(CreateAPIView):
    serializer_class = UserModelSerializer
    queryset = User.objects.all()

# 设置路由
# 子应用路由 urls.py
urlpatterns=[
    ...
    path(r'register/', views.UserAPIView.as_view() ),
]

 

 

 错误集:

# 发送短信,返回一直是-1失败的原因,在sms里面打印result 信息即可知道如何解决

# 403 Forbidden ulrs/register
讲dev里面的crsf注册掉,并清理缓存即可

 

3.  课程列表页

前端显示课程列表页面

在xadmin中增加课程列表的链接导航。然后把课程列表页面组件Course.vue放到项目中.

 

<template>
  <div class="course">
    <Header/>
    <div class="main">
      <!-- 筛选功能 -->
      <div class="top">
        <ul class="condition condition1">
          <li class="cate-condition">课程分类:</li>
          <li class="item current">全部</li>
          <li class="item">Python</li>
          <li class="item">Linux运维</li>
          <li class="item">Python进阶</li>
          <li class="item">开发工具</li>
          <li class="item">Go语言</li>
          <li class="item">机器学习</li>
          <li class="item">技术生涯</li>
        </ul>
        <ul class="condition condition2">
          <li class="cate-condition">筛&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;选:</li>
          <li class="item current">默认</li>
          <li class="item">人气</li>
          <li class="item price">价格</li>
          <li class="course-length">共21个课程</li>
        </ul>
      </div>
      <!-- 课程列表 --->
      <div class="list">
        <ul>
          <li class="course-item">
            <router-link to="/detail" class="course-link">
              <div class="course-cover">
                <img src="../../static/course/1544059695.jpeg" alt="">
              </div>
              <div class="course-info">
                <div class="course-title">
                  <h3>Python开发21天入门</h3>
                  <span>46520人已加入学习</span>
                </div>
                <p class="teacher">
                  <span class="info">Alex 金角大王 老男孩Python教学总监</span>
                  <span class="lesson">共154课时/更新完成</span>
                </p>
                <ul class="lesson-list">
                  <li>
                    <p class="lesson-title">01 | 常用模块学习-模块的种类和导入方法</p>
                    <span class="free">免费</span>
                  </li>
                  <li>
                    <p class="lesson-title">02 | 编程语言介绍(三)高级语言</p>
                    <span class="free">免费</span>
                  </li>
                  <li>
                    <p class="lesson-title">03 | 编程语言介绍(一)</p>
                    <span class="free">免费</span>
                  </li>
                  <li>
                    <p class="lesson-title">04 | 课程介绍(二)-Python与其他语言的区别</p>
                    <span class="free">免费</span>
                  </li>
                </ul>
                <div class="buy-info">
                  <span class="discount">限时免费</span>
                  <span class="present-price">¥0.00元</span>
                  <span class="original-price">原价:9.00元</span>
                  <button class="buy-now">立即购买</button>
                </div>
              </div>
            </router-link>
          </li>
          <li class="course-item">
            <div class="course-cover">
              <img src="../../static/course/1544059695.jpeg" alt="">
            </div>
            <div class="course-info">
              <div class="course-title">
                <h3>Python开发21天入门</h3>
                <span>46520人已加入学习</span>
              </div>
              <p class="teacher">
                <span class="info">Alex 金角大王 老男孩Python教学总监</span>
                <span class="lesson">共154课时/更新完成</span>
              </p>
              <ul class="lesson-list">
                <li>
                  <p class="lesson-title">01 | 常用模块学习-模块的种类和导入方法</p>
                  <span class="free">免费</span>
                </li>
                <li>
                  <p class="lesson-title">02 | 编程语言介绍(三)高级语言</p>
                  <span class="free">免费</span>
                </li>
                <li>
                  <p class="lesson-title">03 | 编程语言介绍(一)</p>
                  <span class="free">免费</span>
                </li>
                <li>
                  <p class="lesson-title">04 | 课程介绍(二)-Python与其他语言的区别</p>
                  <span class="free">免费</span>
                </li>
              </ul>
              <div class="buy-info">
                <span class="discount">限时免费</span>
                <span class="present-price">¥0.00元</span>
                <span class="original-price">原价:9.00元</span>
                <button class="buy-now">立即购买</button>
              </div>
            </div>
          </li>
          <li class="course-item">
            <div class="course-cover">
              <img src="../../static/course/1544059695.jpeg" alt="">
            </div>
            <div class="course-info">
              <div class="course-title">
                <h3>Python开发21天入门</h3>
                <span>46520人已加入学习</span>
              </div>
              <p class="teacher">
                <span class="info">Alex 金角大王 老男孩Python教学总监</span>
                <span class="lesson">共154课时/更新完成</span>
              </p>
              <ul class="lesson-list">
                <li>
                  <p class="lesson-title">01 | 常用模块学习-模块的种类和导入方法</p>
                  <span class="free">免费</span>
                </li>
                <li>
                  <p class="lesson-title">02 | 编程语言介绍(三)高级语言</p>
                  <span class="free">免费</span>
                </li>
                <li>
                  <p class="lesson-title">03 | 编程语言介绍(一)</p>
                  <span class="free">免费</span>
                </li>
                <li>
                  <p class="lesson-title">04 | 课程介绍(二)-Python与其他语言的区别</p>
                  <span class="free">免费</span>
                </li>
              </ul>
              <div class="buy-info">
                <span class="discount">限时免费</span>
                <span class="present-price">¥0.00元</span>
                <span class="original-price">原价:9.00元</span>
                <button class="buy-now">立即购买</button>
              </div>
            </div>
          </li>
          <li class="course-item">
            <div class="course-cover">
              <img src="../../static/course/1544059695.jpeg" alt="">
            </div>
            <div class="course-info">
              <div class="course-title">
                <h3>Python开发21天入门</h3>
                <span>46520人已加入学习</span>
              </div>
              <p class="teacher">
                <span class="info">Alex 金角大王 老男孩Python教学总监</span>
                <span class="lesson">共154课时/更新完成</span>
              </p>
              <ul class="lesson-list">
                <li>
                  <p class="lesson-title">01 | 常用模块学习-模块的种类和导入方法</p>
                  <span class="free">免费</span>
                </li>
                <li>
                  <p class="lesson-title">02 | 编程语言介绍(三)高级语言</p>
                  <span class="free">免费</span>
                </li>
                <li>
                  <p class="lesson-title">03 | 编程语言介绍(一)</p>
                  <span class="free">免费</span>
                </li>
                <li>
                  <p class="lesson-title">04 | 课程介绍(二)-Python与其他语言的区别</p>
                  <span class="free">免费</span>
                </li>
              </ul>
              <div class="buy-info">
                <span class="discount">限时免费</span>
                <span class="present-price">¥0.00元</span>
                <span class="original-price">原价:9.00元</span>
                <button class="buy-now">立即购买</button>
              </div>
            </div>
          </li>
        </ul>
      </div>
    </div>
    <Footer/>
  </div>
</template>

<script>
import Header from "./common/Header"
import Footer from "./common/Footer"
export default {
  name: "Course",
  data(){
    return {
    }
  },

  components:{Header,Footer}
}
</script>

<style scoped>
.main{
    width: 1100px;
    height: auto;
    margin: 0 auto;
    padding-top: 35px;
}
.main .top{
    margin-bottom: 35px;
    padding: 25px 30px 25px 20px;
    background: #fff;
    border-radius: 4px;
    box-shadow: 0 2px 4px 0 #f0f0f0;
}
.condition{
    border-bottom: 1px solid #333;
    border-bottom-color: rgba(51,51,51,.05);
    padding-bottom: 18px;
    margin-bottom: 17px;
    overflow: hidden;
}
.condition li{
    float: left;
}
.condition .cate-condition{
    color: #888;
    font-size: 16px;
}
.condition .item{
    padding: 6px 16px;
    line-height: 16px;
    margin-left: 14px;
    position: relative;
    transition: all .3s ease;
    cursor: pointer;
    color: #4a4a4a;
}
.condition1 .current{
    color: #ffc210;
    border: 1px solid #ffc210!important;
    border-radius: 30px;
}
.condition2 .current{
  color: #ffc210;
}
.condition .price:before{
    content: "";
    width: 0;
    border: 5px solid transparent;
    border-top-color: #d8d8d8;
    position: absolute;
    right: 0;
    bottom: 2.5px;
}
.condition .price:after{
    content: "";
    width: 0;
    border: 5px solid transparent;
    border-bottom-color: #ffc210;
    position: absolute;
    right: 0;
    top: 2.5px;
}
.condition2 .course-length{
    float: right;
    font-size: 14px;
    color: #9b9b9b;
}
.course-item{
    background: #fff;
    padding: 20px 30px 20px 20px;
    margin-bottom: 35px;
    border-radius: 2px;
    cursor: pointer;
    box-shadow: 2px 3px 16px rgba(0,0,0,.1);
    transition: all .2s ease;
    overflow: hidden;
    cursor:pointer;
}
.course-link{
  overflow: hidden;
}
.course-cover {
    width: 423px;
    height: 210px;
    margin-right: 30px;
    float: left;
}
.course-info{
    width: 597px;
    float: left;
}
.course-title{
  margin-bottom: 8px;
  overflow: hidden;

}
.course-title h3{
  font-size: 26px;
  color: #333;
  float: left;
}
.course-title span {
  float: right;
  font-size: 14px;
  color: #9b9b9b;
  margin-top: 12px;
  text-indent: 1em; /* 缩进 2字符宽度 */
  background: url("../assets/people.svg") no-repeat 0px 3px;
}
.teacher{
    justify-content: space-between;
    font-size: 14px;
    color: #9b9b9b;
    margin-bottom: 14px;
    padding-bottom: 14px;
    border-bottom: 1px solid #333;
    border-bottom-color: rgba(51,51,51,.05);
}
.teacher .lesson{
    float: right;
}
.lesson-list{
  overflow: hidden;
}
.lesson-list li{
    width: 49%;
    margin-bottom: 15px;
    cursor: pointer;
    float: left;
    margin-right:1%;
}
.lesson-list li .player{
    width: 16px;
    height: 16px;
    vertical-align: text-bottom;
}
.lesson-list li .lesson-title {
  display: inline-block;
  max-width: 227px;
  text-overflow: ellipsis; /* 如果字体太多超出元素的宽度,则添加省略符号 */
  color: #666;
  overflow: hidden;
  white-space: nowrap;
  font-size: 14px;
  vertical-align: text-bottom; /* 文本的垂直对齐方式: text-botton 文本底部对齐 */
  text-indent: 1.5em;
  background: url(../../static/player.svg) no-repeat 0px 3px;
}

.lesson-list .free{
    width: 34px;
    height: 20px;
    color: #fd7b4d;
    margin-left: 10px;
    border: 1px solid #fd7b4d;
    border-radius: 2px;
    text-align: center;
    font-size: 13px;
    white-space: nowrap;
}
.lesson-list li:hover .lesson-title{
    color: #ffc210;
    background-image: url(../../static/player2.svg);
}
.lesson-list li:hover .free{
    border-color: #ffc210;
    color: #ffc210;
}

.buy-info .discount{
    padding: 0px 10px;
    font-size: 16px;
    color: #fff;
    display: inline-block;
    height: 36px;
    text-align: center;
    margin-right: 8px;
    background: #fa6240;
    border: 1px solid #fa6240;
    border-radius: 10px 0 10px 0;
    line-height: 36px;
}
.present-price{
    font-size: 24px;
    color: #fa6240;
}
.original-price{
    text-decoration: line-through;
    font-size: 14px;
    color: #9b9b9b;
    margin-left: 10px;
}
.buy-now{
    width: 120px;
    height: 38px;
    background: transparent;
    color: #fa6240;
    font-size: 16px;
    border: 1px solid #fd7b4d;
    border-radius: 3px;
    transition: all .2s ease-in-out; /* 过渡动画 */
    float: right;
    margin-top: 5px;
}
.buy-now:hover{
    color: #fff;
    background: #ffc210;
    border: 1px solid #ffc210;
    cursor: pointer;
}
</style>

 

注册路由

import Course from "../components/Course"

 ....
    {
      name:"Course",
      path: "/course",
      component: Course,
    }

 

数据模型创建

 

from django.db import models
from luffy.utils.models import BaseModel
# 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



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="付费类型")
    # 使用这个字段的原因
    brief = models.TextField(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 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]


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)

 

python manage.py makemigrations
python manage.py migrate

 

执行数据库时,如果发生错误

 则到 django/db/backends/utils.py : 237,修改代码如下:

    if value is None:
        return None
    if isinstance(value, decimal.Decimal):
        context = decimal.getcontext().copy()
        if max_digits is not None:
            # 增加转型类型转换
            context.prec = int(max_digits)
        if decimal_places is not None:
            # 增加转型类型转换
            value = value.quantize(decimal.Decimal(1).scaleb(-int(decimal_places)), context=context)
        else:
            context.traps[decimal.Rounded] = 1
            value = context.create_decimal(value)
        return "{:f}".format(value)
    if decimal_places is not None:
        return "%.*f" % (decimal_places, value)
    return "{:f}".format(value)

修改完记得删掉ly_users 里面的其它数据,仅留下root做登录测试用

把当前新增的课程模型注册到xadmin里面.

import xadmin

from .models import CourseCategory
class CourseCategoryModelAdmin(object):
    """课程分类模型管理类"""
    pass
xadmin.site.register(CourseCategory, CourseCategoryModelAdmin)


from .models import Course
class CourseModelAdmin(object):
    """课程模型管理类"""
    pass
xadmin.site.register(Course, CourseModelAdmin)


from .models import Teacher
class TeacherModelAdmin(object):
    """老师模型管理类"""
    pass
xadmin.site.register(Teacher, TeacherModelAdmin)


from .models import CourseChapter
class CourseChapterModelAdmin(object):
    """课程章节模型管理类"""
    pass
xadmin.site.register(CourseChapter, CourseChapterModelAdmin)



from .models import CourseLesson
class CourseLessonModelAdmin(object):
    """课程课时模型管理类"""
    pass
xadmin.site.register(CourseLesson, CourseLessonModelAdmin)

 

添加测试数据

 

后端实现课程分列列表接口:

创建序列化器

# courses/serializers.py
from rest_framework import serializers
from .models import CourseCategory
class CourseCategoryModelSerializer(serializers.ModelSerializer):
    class Meta:
        model = CourseCategory
        fields = ("id","name")

 

创建视图

# courses/views.py
# Create your views here.
from rest_framework.generics import ListAPIView
from .models import CourseCategory
from .serializers import CourseCategoryModelSerializer

class CourseCategoryAPIView(ListAPIView):
    """课程分类列表"""
    queryset = CourseCategory.objects.filter( is_delete=False, is_show=True ).order_by("-orders")
    serializer_class = CourseCategoryModelSerializer

 

注册路由

from django.urls import path
from . import views
urlpatterns = [
    path(r"cate/",views.CourseCategoryAPIView.as_view()),
]



# 总路由
    path('courses/', include("courses.urls")),

 

客户端发送请求获取课程分类信息

<template>
  <div class="course">
    <Header/>
    <div class="main">
      <!-- 筛选功能 -->
      <div class="top">
        <ul class="condition condition1">
          <li class="cate-condition">课程分类:</li>
          <li class="item current">全部</li>
          <li 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 current">默认</li>
          <li class="item">人气</li>
          <li class="item price">价格</li>
          <li class="course-length">共21个课程</li>
        </ul>
      </div>
      。。。。
  </div>
</template>

<script>
import Header from "./common/Header"
import Footer from "./common/Footer"
export default {
  name: "Course",
  data(){
    return {
      catetory_list:[],
    }
  },
  components: {Header, Footer},
  created(){
    // 获取课程分类
    this.$axios.get(this.$settings.Host+"/courses/cate/").then(response=>{
      this.catetory_list = response.data
    }).catch(error=>{
      console.log(error.response)
    });
  },
}
</script>

 

完善课程分类筛选的前端特效

<template>
  <div class="course">
    <Header/>
    <div class="main">
      <!-- 筛选功能 -->
      <div class="top">
        <ul class="condition condition1">
          <li class="cate-condition">课程分类:</li>
          <li class="item" :class="current_cate==0?'current':''" @click="current_cate=0">全部</li>
          <li :class="current_cate==catetory.id?'current':''" @click="current_cate=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 current">默认</li>
          <li class="item">人气</li>
          <li class="item price">价格</li>
          <li class="course-length">共21个课程</li>
        </ul>
      </div>
      ....
  </div>
</template>

<script>
import Header from "./common/Header"
import Footer from "./common/Footer"
export default {
  name: "Course",
  data(){
    return {
      catetory_list:[],
      current_cate:0,
    }
  },
  components: {Header, Footer},
  created(){
    // 获取课程分类
    this.$axios.get(this.$settings.Host+"/courses/cate/").then(response=>{
      this.catetory_list = response.data
    }).catch(error=>{
      console.log(error.response)
    });
  },
  methods:{

  }
}
</script>

<style scoped>

.condition .item{
    padding: 6px 16px;
    line-height: 16px;
    margin-left: 14px;
    position: relative;
    transition: all .3s ease;
    border:1px solid transparent; /*  transparent 透明 */
    cursor: pointer;
    color: #4a4a4a;
}
    
...
</style>

 

后端实现课程信息列表接口

# 创建序列化器,代码
# 开发中一个序列化器 A 中需要同时序列化其他模型 B 的数据返回给客户端,那么直接通过外键默认只会返回主键ID
# 所以我们可以通过再创建一个模型B的序列化器,对模型B的数据进行序列化
# 在序列化器A中直接把模型B的序列化器调用作为字段值来声明即可.

from .models import Teacher
class TeacherSerializer(serializers.ModelSerializer):
    class Meta:
        model = Teacher
        fields = ("id","name","title")

from .models import Course
class CourseSerializer(serializers.ModelSerializer):
    # 这里调用的序列化器,必须事先在前面已经声明好的,否则报错
    teacher = TeacherSerializer()
    class Meta:
        model= Course
        fields = ("id","name","course_img","students","lessons","pub_lessons","price","teacher")

 

# 视图代码:
from .serializers import CourseModelSerializer
class CourseAPIView(ListAPIView):
    queryset = Course.objects.filter(is_delete=False, is_show=True).order_by("orders")
    serializer_class = CourseModelSerializer

# 路由代码:
path(r"list/",views.CourseAPIView.as_view()),

 

客户端发送请求获取课程列表信息

<template>
  <div class="course">
    <Header/>
    <div class="main">
      <!-- 筛选功能 -->
      <div class="top">
        <ul class="condition condition1">
          <li class="cate-condition">课程分类:</li>
          <li class="item" :class="current_cate==0?'current':''" @click="current_cate=0">全部</li>
          <li :class="current_cate==catetory.id?'current':''" @click="current_cate=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 current">默认</li>
          <li class="item">人气</li>
          <li class="item 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="/detail" 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>
                    <p class="lesson-title">01 | 常用模块学习-模块的种类和导入方法</p>
                    <span class="free">免费</span>
                  </li>
                  <li>
                    <p class="lesson-title">02 | 编程语言介绍(三)高级语言</p>
                    <span class="free">免费</span>
                  </li>
                  <li>
                    <p class="lesson-title">03 | 编程语言介绍(一)</p>
                    <span class="free">免费</span>
                  </li>
                  <li>
                    <p class="lesson-title">04 | 课程介绍(二)-Python与其他语言的区别</p>
                    <span class="free">免费</span>
                  </li>
                </ul>
                <div class="buy-info">
                  <span class="discount">限时免费</span>
                  <span class="present-price">¥0.00元</span>
                  <span class="original-price">原价:{{course.price}}元</span>
                  <button class="buy-now">立即购买</button>
                </div>
              </div>
            </router-link>
          </li>
<!--          <li class="course-item">-->
<!--            <div class="course-cover">-->
<!--              <img src="../../static/course/1544059695.jpeg" alt="">-->
<!--            </div>-->
<!--            <div class="course-info">-->
<!--              <div class="course-title">-->
<!--                <h3>Python开发21天入门</h3>-->
<!--                <span>46520人已加入学习</span>-->
<!--              </div>-->
<!--              <p class="teacher">-->
<!--                <span class="info">Alex 金角大王 老男孩Python教学总监</span>-->
<!--                <span class="lesson">共154课时/更新完成</span>-->
<!--              </p>-->
<!--              <ul class="lesson-list">-->
<!--                <li>-->
<!--                  <p class="lesson-title">01 | 常用模块学习-模块的种类和导入方法</p>-->
<!--                  <span class="free">免费</span>-->
<!--                </li>-->
<!--                <li>-->
<!--                  <p class="lesson-title">02 | 编程语言介绍(三)高级语言</p>-->
<!--                  <span class="free">免费</span>-->
<!--                </li>-->
<!--                <li>-->
<!--                  <p class="lesson-title">03 | 编程语言介绍(一)</p>-->
<!--                  <span class="free">免费</span>-->
<!--                </li>-->
<!--                <li>-->
<!--                  <p class="lesson-title">04 | 课程介绍(二)-Python与其他语言的区别</p>-->
<!--                  <span class="free">免费</span>-->
<!--                </li>-->
<!--              </ul>-->
<!--              <div class="buy-info">-->
<!--                <span class="discount">限时免费</span>-->
<!--                <span class="present-price">¥0.00元</span>-->
<!--                <span class="original-price">原价:9.00元</span>-->
<!--                <button class="buy-now">立即购买</button>-->
<!--              </div>-->
<!--            </div>-->
<!--          </li>-->
<!--          <li class="course-item">-->
<!--            <div class="course-cover">-->
<!--              <img src="../../static/course/1544059695.jpeg" alt="">-->
<!--            </div>-->
<!--            <div class="course-info">-->
<!--              <div class="course-title">-->
<!--                <h3>Python开发21天入门</h3>-->
<!--                <span>46520人已加入学习</span>-->
<!--              </div>-->
<!--              <p class="teacher">-->
<!--                <span class="info">Alex 金角大王 老男孩Python教学总监</span>-->
<!--                <span class="lesson">共154课时/更新完成</span>-->
<!--              </p>-->
<!--              <ul class="lesson-list">-->
<!--                <li>-->
<!--                  <p class="lesson-title">01 | 常用模块学习-模块的种类和导入方法</p>-->
<!--                  <span class="free">免费</span>-->
<!--                </li>-->
<!--                <li>-->
<!--                  <p class="lesson-title">02 | 编程语言介绍(三)高级语言</p>-->
<!--                  <span class="free">免费</span>-->
<!--                </li>-->
<!--                <li>-->
<!--                  <p class="lesson-title">03 | 编程语言介绍(一)</p>-->
<!--                  <span class="free">免费</span>-->
<!--                </li>-->
<!--                <li>-->
<!--                  <p class="lesson-title">04 | 课程介绍(二)-Python与其他语言的区别</p>-->
<!--                  <span class="free">免费</span>-->
<!--                </li>-->
<!--              </ul>-->
<!--              <div class="buy-info">-->
<!--                <span class="discount">限时免费</span>-->
<!--                <span class="present-price">¥0.00元</span>-->
<!--                <span class="original-price">原价:9.00元</span>-->
<!--                <button class="buy-now">立即购买</button>-->
<!--              </div>-->
<!--            </div>-->
<!--          </li>-->
<!--          <li class="course-item">-->
<!--            <div class="course-cover">-->
<!--              <img src="../../static/course/1544059695.jpeg" alt="">-->
<!--            </div>-->
<!--            <div class="course-info">-->
<!--              <div class="course-title">-->
<!--                <h3>Python开发21天入门</h3>-->
<!--                <span>46520人已加入学习</span>-->
<!--              </div>-->
<!--              <p class="teacher">-->
<!--                <span class="info">Alex 金角大王 老男孩Python教学总监</span>-->
<!--                <span class="lesson">共154课时/更新完成</span>-->
<!--              </p>-->
<!--              <ul class="lesson-list">-->
<!--                <li>-->
<!--                  <p class="lesson-title">01 | 常用模块学习-模块的种类和导入方法</p>-->
<!--                  <span class="free">免费</span>-->
<!--                </li>-->
<!--                <li>-->
<!--                  <p class="lesson-title">02 | 编程语言介绍(三)高级语言</p>-->
<!--                  <span class="free">免费</span>-->
<!--                </li>-->
<!--                <li>-->
<!--                  <p class="lesson-title">03 | 编程语言介绍(一)</p>-->
<!--                  <span class="free">免费</span>-->
<!--                </li>-->
<!--                <li>-->
<!--                  <p class="lesson-title">04 | 课程介绍(二)-Python与其他语言的区别</p>-->
<!--                  <span class="free">免费</span>-->
<!--                </li>-->
<!--              </ul>-->
<!--              <div class="buy-info">-->
<!--                <span class="discount">限时免费</span>-->
<!--                <span class="present-price">¥0.00元</span>-->
<!--                <span class="original-price">原价:9.00元</span>-->
<!--                <button class="buy-now">立即购买</button>-->
<!--              </div>-->
<!--            </div>-->
<!--          </li>-->
        </ul>
      </div>
    </div>
    <Footer/>
  </div>
</template>

<script>
import Header from "./common/Header"
import Footer from "./common/Footer"
export default {
  name: "Course",
  data(){
    return {
      catetory_list:[],
      course_list:[],
      current_cate:0,
    }
  },
  components: {Header, Footer},
  created(){
    // 获取课程分类
    this.$axios.get(this.$settings.Host+"/courses/cate/").then(response=>{
      this.catetory_list = response.data
    }).catch(error=>{
      console.log(error.response)
    });

    // 获取课程信息
    this.$axios.get(this.$settings.Host+"/courses/list/").then(response=>{
      this.course_list = response.data
    }).catch(error=>{
      console.log(error.response)
    });

  },
  methods:{

  }
}
</script>

<style scoped>
.main{
    width: 1100px;
    height: auto;
    margin: 0 auto;
    padding-top: 35px;
}
.main .top{
    margin-bottom: 35px;
    padding: 25px 30px 25px 20px;
    background: #fff;
    border-radius: 4px;
    box-shadow: 0 2px 4px 0 #f0f0f0;
}
.condition{
    border-bottom: 1px solid #333;
    border-bottom-color: rgba(51,51,51,.05);
    padding-bottom: 18px;
    margin-bottom: 17px;
    overflow: hidden;
}
.condition li{
    float: left;
}
.condition .cate-condition{
    color: #888;
    font-size: 16px;
}
.condition .item{
    padding: 6px 16px;
    line-height: 16px;
    margin-left: 14px;
    position: relative;
    transition: all .3s ease;
    border:1px solid transparent; /*  transparent 透明 */
    cursor: pointer;
    color: #4a4a4a;
}
.condition1 .current{
    color: #ffc210;
    border: 1px solid #ffc210!important;
    border-radius: 30px;
}
.condition2 .current{
  color: #ffc210;
}
.condition .price:before{
    content: "";
    width: 0;
    border: 5px solid transparent;
    border-top-color: #d8d8d8;
    position: absolute;
    right: 0;
    bottom: 2.5px;
}
.condition .price:after{
    content: "";
    width: 0;
    border: 5px solid transparent;
    border-bottom-color: #ffc210;
    position: absolute;
    right: 0;
    top: 2.5px;
}
.condition2 .course-length{
    float: right;
    font-size: 14px;
    color: #9b9b9b;
}
.course-item{
    background: #fff;
    padding: 20px 30px 20px 20px;
    margin-bottom: 35px;
    border-radius: 2px;
    cursor: pointer;
    box-shadow: 2px 3px 16px rgba(0,0,0,.1);
    transition: all .2s ease;
    overflow: hidden;
    cursor:pointer;
}
.course-link{
  overflow: hidden;
}
.course-cover {
    width: 423px;
    height: 210px;
    margin-right: 30px;
    float: left;
}
.course-info{
    width: 597px;
    float: left;
}
.course-title{
  margin-bottom: 8px;
  overflow: hidden;

}
.course-title h3{
  font-size: 26px;
  color: #333;
  float: left;
}
.course-title span {
  float: right;
  font-size: 14px;
  color: #9b9b9b;
  margin-top: 12px;
  text-indent: 1em; /* 缩进 2字符宽度 */
  background: url("../assets/people.svg") no-repeat 0px 3px;
}
.teacher{
    justify-content: space-between;
    font-size: 14px;
    color: #9b9b9b;
    margin-bottom: 14px;
    padding-bottom: 14px;
    border-bottom: 1px solid #333;
    border-bottom-color: rgba(51,51,51,.05);
}
.teacher .lesson{
    float: right;
}
.lesson-list{
  overflow: hidden;
}
.lesson-list li{
    width: 49%;
    margin-bottom: 15px;
    cursor: pointer;
    float: left;
    margin-right:1%;
}
.lesson-list li .player{
    width: 16px;
    height: 16px;
    vertical-align: text-bottom;
}
.lesson-list li .lesson-title {
  display: inline-block;
  max-width: 227px;
  text-overflow: ellipsis; /* 如果字体太多超出元素的宽度,则添加省略符号 */
  color: #666;
  overflow: hidden;
  white-space: nowrap;
  font-size: 14px;
  vertical-align: text-bottom; /* 文本的垂直对齐方式: text-botton 文本底部对齐 */
  text-indent: 1.5em;
  background: url(../../static/player.svg) no-repeat 0px 3px;
}

.lesson-list .free{
    width: 34px;
    height: 20px;
    color: #fd7b4d;
    margin-left: 10px;
    border: 1px solid #fd7b4d;
    border-radius: 2px;
    text-align: center;
    font-size: 13px;
    white-space: nowrap;
}
.lesson-list li:hover .lesson-title{
    color: #ffc210;
    background-image: url(../../static/player2.svg);
}
.lesson-list li:hover .free{
    border-color: #ffc210;
    color: #ffc210;
}

.buy-info .discount{
    padding: 0px 10px;
    font-size: 16px;
    color: #fff;
    display: inline-block;
    height: 36px;
    text-align: center;
    margin-right: 8px;
    background: #fa6240;
    border: 1px solid #fa6240;
    border-radius: 10px 0 10px 0;
    line-height: 36px;
}
.present-price{
    font-size: 24px;
    color: #fa6240;
}
.original-price{
    text-decoration: line-through;
    font-size: 14px;
    color: #9b9b9b;
    margin-left: 10px;
}
.buy-now{
    width: 120px;
    height: 38px;
    background: transparent;
    color: #fa6240;
    font-size: 16px;
    border: 1px solid #fd7b4d;
    border-radius: 3px;
    transition: all .2s ease-in-out; /* 过渡动画 */
    float: right;
    margin-top: 5px;
}
.buy-now:hover{
    color: #fff;
    background: #ffc210;
    border: 1px solid #ffc210;
    cursor: pointer;
}
</style>

 

 

客户端的课程列表显示当前课程的章节信息

# 可以通过序列化器嵌套来完成,但是查询过程的数量不好控制。以下代码仅供参考:
from .models import CourseLesson
class CourseLessonModelSerializer(serializers.ModelSerializer):
    class Meta:
        model = CourseLesson
        fields = ("id","name","free_trail")

from .models import CourseChapter
class CourseChapterModelSerializer(serializers.ModelSerializer):
    coursesections = CourseLessonModelSerializer(many=True)
    class Meta:
        model = CourseChapter
        fields = ("id", "name", "coursesections")

from .models import Course
class CourseModelSerializer(serializers.ModelSerializer):
    teacher = TeacherModelSerializer()
    coursechapters = CourseChapterModelSerializer(many=True)  # 课程章节多个,所以需要声明 many=True
    class Meta:
        model = Course
        fields = ("id", "name", "course_img", "students", "lessons", "pub_lessons", "price", "teacher","coursechapters")

# 我们可以通过在models模型中给数据模型增加自定义字段来完成获取课时功能。
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="付费类型")
    # 使用这个字段的原因
    brief = models.TextField(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 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]

 

# 序列化器字段增加自定义的模型字段,
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")

 

前端显示课程章节信息

<template>
  <div class="course">
    <Header/>
    <div class="main">
      <!-- 筛选功能 -->
      <div class="top">
        <ul class="condition condition1">
          <li class="cate-condition">课程分类:</li>
          <li class="item" :class="current_cate==0?'current':''" @click="current_cate=0">全部</li>
          <li :class="current_cate==catetory.id?'current':''" @click="current_cate=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 current">默认</li>
          <li class="item">人气</li>
          <li class="item 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="/detail" 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">
                  <span class="discount">限时免费</span>
                  <span class="present-price">¥0.00元</span>
                  <span class="original-price">原价:{{course.price}}元</span>
                  <button class="buy-now">立即购买</button>
                </div>
              </div>
            </router-link>
          </li>
        </ul>
      </div>
    </div>
    <Footer/>
  </div>
</template>

<script>
import Header from "./common/Header"
import Footer from "./common/Footer"
export default {
  name: "Course",
  data(){
    return {
      catetory_list:[],
      course_list:[],
      current_cate:0,
    }
  },
  components: {Header, Footer},
  created(){
    // 获取课程分类
    this.$axios.get(this.$settings.Host+"/courses/cate/").then(response=>{
      this.catetory_list = response.data
    }).catch(error=>{
      console.log(error.response)
    });

    // 获取课程信息
    this.$axios.get(this.$settings.Host+"/courses/list/").then(response=>{
      this.course_list = response.data
    }).catch(error=>{
      console.log(error.response)
    });

  },
  methods:{

  }
}
</script>

 

按照指定分类显示课程信息

# 在当前项目中安装 字段过滤排序
pip install django-filter

# 在settings/dev.py配置文件中增加过滤后端的设置:
INSTALLED_APPS = [
    ...
    'django_filters',  # 需要注册应用,
]

# 在视图中设置允许过滤的字段名和引入过滤字段核心类
from .serializers import CourseModelSerializer
from django_filters.rest_framework import DjangoFilterBackend
class CourseAPIView(ListAPIView):
    queryset = Course.objects.filter(is_delete=False, is_show=True).order_by("orders")
    serializer_class = CourseModelSerializer
    filter_backends = [DjangoFilterBackend, ]
    filter_fields = ('course_category',)

 

排序显示课程

后端提供排序课程的接口

# 把原来views.py中的CoursesAPIView新增两句代码:
from .models import Course
from .serializers import CourseModelSerializer
from rest_framework.filters import OrderingFilter
from django_filters.rest_framework import DjangoFilterBackend
class CourseAPIView(ListAPIView):
    queryset = Course.objects.filter(status=0).order_by("-orders","-id")
    serializer_class = CourseModelSerializer
    # 设置价格排序
    filter_backends = [DjangoFilterBackend,OrderingFilter]
    filter_fields = ('course_category',)
    ordering_fields = ('id', 'students', 'price')

 

前端根据排序字段对应的课程顺序

# 组件html代码
<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="/detail" 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">
                  <span class="discount">限时免费</span>
                  <span class="present-price">¥0.00元</span>
                  <span class="original-price">原价:{{course.price}}元</span>
                  <button class="buy-now">立即购买</button>
                </div>
              </div>
            </router-link>
          </li>
        </ul>
      </div>
    </div>
    <Footer/>
  </div>
</template>

<script>
import Header from "./common/Header"
import Footer from "./common/Footer"
export default {
  name: "Course",
  data(){
    return {
      catetory_list:[],
      course_list:[],
      query_params:{
        course_category: 0,
        ordering:"-id",
      }
    }
  },
  watch:{
    // 每次点击不同课程时,要重新获取课程列表
    "query_params.course_category":function(){
       this.get_course_list();
    },
    "query_params.ordering":function(){
       this.get_course_list();
    },
  },
  components: {Header, Footer},
  created(){
    // 获取课程分类
    this.$axios.get(this.$settings.Host+"/courses/cate/").then(response=>{
      this.catetory_list = response.data
    }).catch(error=>{
      console.log(error.response)
    });

    // 获取课程信息
    this.get_course_list()

  },
  methods:{
    select_ordering(selector){
      // 默认排序
      if(this.query_params.ordering==('-'+selector) ){
        this.query_params.ordering = selector;
      }else{
        this.query_params.ordering = '-'+selector;
      }
    },
    get_course_list(){
      let query_params = {ordering:this.query_params.ordering};

      if( this.query_params.course_category != 0 ){
        query_params.course_category = this.query_params.course_category;
      }

      this.$axios.get(this.$settings.Host+"/courses/list/",{
        params: query_params
      }).then(response=>{
        this.course_list = response.data
      }).catch(error=>{
        console.log(error.response)
      });
    }
  }
}
</script>

<style scoped>
.main{
    width: 1100px;
    height: auto;
    margin: 0 auto;
    padding-top: 35px;
}
.main .top{
    margin-bottom: 35px;
    padding: 25px 30px 25px 20px;
    background: #fff;
    border-radius: 4px;
    box-shadow: 0 2px 4px 0 #f0f0f0;
}
.condition{
    border-bottom: 1px solid #333;
    border-bottom-color: rgba(51,51,51,.05);
    padding-bottom: 18px;
    margin-bottom: 17px;
    overflow: hidden;
}
.condition li{
    float: left;
}
.condition .cate-condition{
    color: #888;
    font-size: 16px;
}
.condition .item{
    padding: 6px 16px;
    line-height: 16px;
    margin-left: 14px;
    position: relative;
    transition: all .3s ease;
    border:1px solid transparent; /*  transparent 透明 */
    cursor: pointer;
    color: #4a4a4a;
}
.condition1 .current{
    color: #ffc210;
    border: 1px solid #ffc210!important;
    border-radius: 30px;
}
.condition2 .current{
  color: #ffc210;
}
.condition .price:before{
    content: "";
    width: 0;
    border: 5px solid transparent;
    border-top-color: #d8d8d8;
    position: absolute;
    right: 0;
    bottom: 2.5px;
}
.condition .price2:before{
    content: "";
    width: 0;
    border: 5px solid transparent;
    position: absolute;
    right: 0;
    bottom: 2.5px;
    border-top-color: #ffc210;
}
.condition .price2:after{
    content: "";
    width: 0;
    border: 5px solid transparent;
    position: absolute;
    right: 0;
    top: 2.5px;
    border-bottom-color: #d8d8d8;
}
.condition .price:after{
    content: "";
    width: 0;
    border: 5px solid transparent;
    border-bottom-color: #ffc210;
    position: absolute;
    right: 0;
    top: 2.5px;
}
.condition2 .course-length{
    float: right;
    font-size: 14px;
    color: #9b9b9b;
}
.course-item{
    background: #fff;
    padding: 20px 30px 20px 20px;
    margin-bottom: 35px;
    border-radius: 2px;
    cursor: pointer;
    box-shadow: 2px 3px 16px rgba(0,0,0,.1);
    transition: all .2s ease;
    overflow: hidden;
    cursor:pointer;
}
.course-link{
  overflow: hidden;
}
.course-cover {
    width: 423px;
    height: 210px;
    margin-right: 30px;
    float: left;
}
.course-info{
    width: 597px;
    float: left;
}
.course-title{
  margin-bottom: 8px;
  overflow: hidden;

}
.course-title h3{
  font-size: 26px;
  color: #333;
  float: left;
}
.course-title span {
  float: right;
  font-size: 14px;
  color: #9b9b9b;
  margin-top: 12px;
  text-indent: 1em; /* 缩进 2字符宽度 */
  background: url("../assets/people.svg") no-repeat 0px 3px;
}
.teacher{
    justify-content: space-between;
    font-size: 14px;
    color: #9b9b9b;
    margin-bottom: 14px;
    padding-bottom: 14px;
    border-bottom: 1px solid #333;
    border-bottom-color: rgba(51,51,51,.05);
}
.teacher .lesson{
    float: right;
}
.lesson-list{
  overflow: hidden;
}
.lesson-list li{
    width: 49%;
    margin-bottom: 15px;
    cursor: pointer;
    float: left;
    margin-right:1%;
}
.lesson-list li .player{
    width: 16px;
    height: 16px;
    vertical-align: text-bottom;
}
.lesson-list li .lesson-title {
  display: inline-block;
  max-width: 227px;
  text-overflow: ellipsis; /* 如果字体太多超出元素的宽度,则添加省略符号 */
  color: #666;
  overflow: hidden;
  white-space: nowrap;
  font-size: 14px;
  vertical-align: text-bottom; /* 文本的垂直对齐方式: text-botton 文本底部对齐 */
  text-indent: 1.5em;
  background: url(../../static/player.svg) no-repeat 0px 3px;
}

.lesson-list .free{
    width: 34px;
    height: 20px;
    color: #fd7b4d;
    margin-left: 10px;
    border: 1px solid #fd7b4d;
    border-radius: 2px;
    text-align: center;
    font-size: 13px;
    white-space: nowrap;
}
.lesson-list li:hover .lesson-title{
    color: #ffc210;
    background-image: url(../../static/player2.svg);
}
.lesson-list li:hover .free{
    border-color: #ffc210;
    color: #ffc210;
}

.buy-info .discount{
    padding: 0px 10px;
    font-size: 16px;
    color: #fff;
    display: inline-block;
    height: 36px;
    text-align: center;
    margin-right: 8px;
    background: #fa6240;
    border: 1px solid #fa6240;
    border-radius: 10px 0 10px 0;
    line-height: 36px;
}
.present-price{
    font-size: 24px;
    color: #fa6240;
}
.original-price{
    text-decoration: line-through;
    font-size: 14px;
    color: #9b9b9b;
    margin-left: 10px;
}
.buy-now{
    width: 120px;
    height: 38px;
    background: transparent;
    color: #fa6240;
    font-size: 16px;
    border: 1px solid #fd7b4d;
    border-radius: 3px;
    transition: all .2s ease-in-out; /* 过渡动画 */
    float: right;
    margin-top: 5px;
}
.buy-now:hover{
    color: #fff;
    background: #ffc210;
    border: 1px solid #ffc210;
    cursor: pointer;
}
</style>

 

posted @ 2019-05-14 22:37  pythonernoob  阅读(206)  评论(0)    收藏  举报