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">筛 选:</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">筛 选:</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">筛 选:</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">筛 选:</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">筛 选:</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">筛 选:</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>