课程详细内容页面以及接口
课程详细内容页面以及接口
1 接口api
1 课程相信内容接口
# 访问:http://127.0.0.1:8000/course/4/
# 返回:
{
"id": 4,
"name": "前端课程",
"students": 80,
"sections": 20,
"pub_sections": 15,
"level_name": "高级",
"discount_type": "付费",
"active_time": "2019-12-21 09:05:00",
"real_price": "100.00",
"price": "500.00",
"teacher": {
"name": "Mjj",
"role": 0,
"title": "前美团前端项目组架构师",
"signature": null,
"image": "http://yanapi.oss-cn-beijing.aliyuncs.com/media%2Fteacher%2Fmjj_icon.png",
"brief": "'是马JJ老师, 一个集美貌与才华于一身的男人,搞过几年IOS,又转了前端开发几年,曾就职于美团网任高级前端开发,后来因为不同意王兴(美团老板)的战略布局而出家做老师去了,有丰富的教学经验,开起车来也毫不含糊。一直专注在前端的前沿技术领域。同时,爱好抽烟、喝酒、烫头(锡纸烫)。 我的最爱是前端,因为前端妹子多。"
},
"brief": "这是前端课程去玩儿去玩儿群翁群翁群翁群翁群翁群翁群翁群翁群翁群翁群翁群翁去玩儿去玩儿群翁群翁群翁群翁\r\n这是前端课程去玩儿去玩儿群翁群翁群翁群翁群翁群翁群翁群翁群翁群翁群翁群翁去玩儿去玩儿群翁群翁群翁群翁\r\n这是前端课程去玩儿去玩儿群翁群翁群翁群翁群翁群翁群翁群翁群翁群翁群翁群翁去玩儿去玩儿群翁群翁群翁群翁这是前端课程去玩儿去玩儿群翁群翁群翁群翁群翁群翁群翁群翁群翁群翁群翁群翁去玩儿去玩儿群翁群翁群翁群翁\r\n这是前端课程去玩儿去玩儿群翁群翁群翁群翁群翁群翁群翁群翁群翁群翁群翁群翁去玩儿去玩儿群翁群翁群翁群翁"
}
2 某个课程的章节目录接口
# 访问:http://127.0.0.1:8000/course/chapters/?course=1
# 返回:
[
{
"id": 1,
"name": "计算机原理",
"chapter": 1,
"coursesections": [
{
"name": "计算机原理上",
"free_trail": true,
"orders": 1,
"section_link": null
},
{
"name": "计算机原理下",
"free_trail": true,
"orders": 2,
"section_link": null
}
]
},
{
"id": 2,
"name": "环境搭建",
"chapter": 2,
"coursesections": [
{
"name": "环境搭建上",
"free_trail": false,
"orders": 3,
"section_link": null
},
{
"name": "环境搭建下",
"free_trail": true,
"orders": 4,
"section_link": null
}
]
}
]
2 前端代码
dev.js
export default {
base_url: 'http://127.0.0.1:8000',
playerOptions: {
playbackRates: [0.7, 1.0, 1.5, 2.0], //播放速度
autoplay: false, //如果true,浏览器准备好时开始回放。
muted: false, // 默认情况下将会消除任何音频。
loop: false, // 导致视频一结束就重新开始。
preload: 'auto', // 建议浏览器在<video>加载元素后是否应该开始下载视频数据。auto浏览器选择最佳行为,立即开始加载视频(如果浏览器支持)
language: 'zh-CN',
fluid: true, // 当true时,Video.js player将拥有流体大小。换句话说,它将按比例缩放以适应其容器。
poster: "https://img.zcool.cn/community/012866585b6c43a801219c776b9609.jpg", //你的封面地址
// width: document.documentElement.clientWidth, //播放器宽度
notSupportedMessage: '此视频暂无法播放,请稍后再试', //允许覆盖Video.js无法播放媒体源时显示的默认信息。
controlBar: {
timeDivider: true,//当前时间和持续时间的分隔符
durationDisplay: true,//显示持续时间
remainingTimeDisplay: false,//是否显示剩余时间功能
fullscreenToggle: true, //全屏按钮
},
aspectRatio: '16:9', // 将播放器置于流畅模式,并在计算播放器的动态大小时使用该值。值应该代表一个比例 - 用冒号分隔的两个数字(例如"16:9"或"4:3")
sources: [{ // 播放资源和资源格式
type: "video/mp4",
src: "https://outin-18535a2d206611ea865700163e00b174.oss-cn-shanghai.aliyuncs.com/sv/2e2cd2fe-16f11a4b087/2e2cd2fe-16f11a4b087.mp4?Expires=1576637762&OSSAccessKeyId=LTAI3DkxtsbUyNYV&Signature=6CFkBsBD591CeWqob0FE2ruMb60%3D"//你的视频地址(必填)
}],
}
}
views/course/CoueseDetail.vue
ps: 完善了两条ajax:
1 获取课程内容请求
2 获取章节内容请求
<template>
<div class="detail">
<Header/>
<div class="main">
<div class="course-info">
<div class="wrap-left">
<videoPlayer class="video-player vjs-custom-skin"
ref="videoPlayer"
:playsinline="true"
:options="playerOptions"
@play="onPlayerPlay($event)"
@pause="onPlayerPause($event)">
</videoPlayer>
</div>
<div class="wrap-right">
<h3 class="course-name">{{course_info.name}}</h3>
<p class="data">{{course_info.students}}人在学 课程总时长:{{course_info.sections}}课时/{{course_info.pub_sections}}小时 难度:{{course_info.level_name}}</p>
<div v-if="!(end_time_info.surplus_total_secondse<-1)">
<div class="sale-time">
<p class="sale-type">{{course_info.discount_type}}</p>
<p class="expire">距离结束:仅剩 {{end_time_info.day}}天 {{end_time_info.hour}}小时 {{end_time_info.minute}}分 <span
class="second">{{end_time_info.second}}</span> 秒</p>
</div>
<div ></div>
<div v-if="!(end_time_info.surplus_total_secondse<0)">
<p class="course-price">
<span>活动价</span>
<span class="discount">¥{{course_info.real_price}}</span>
<span class="original">¥{{course_info.price}}</span>
</p>
</div>
</div>
<div v-else class="sale-time">
<p class="sale-type">价格 <span class="original_price">¥{{course_info.price}}</span></p>
<p class="expire"></p>
</div>
<div class="buy">
<div class="buy-btn">
<button class="buy-now">立即购买</button>
<button class="free">免费试学</button>
</div>
<!--<div class="add-cart" @click="add_cart(course_info.id)"><img src="@/assets/img/cart-yellow.svg"-->
<!--alt="">加入购物车-->
<!--</div>-->
</div>
</div>
</div>
<div class="course-tab">
<ul class="tab-list">
<li :class="tabIndex==1?'active':''" @click="tabIndex=1">详情介绍</li>
<li :class="tabIndex==2?'active':''" @click="tabIndex=2">课程章节 <span :class="tabIndex!=2?'free':''">(试学)</span>
</li>
<li :class="tabIndex==3?'active':''" @click="tabIndex=3">用户评论</li>
<li :class="tabIndex==4?'active':''" @click="tabIndex=4">常见问题</li>
</ul>
</div>
<div class="course-content">
<div class="course-tab-list">
<div class="tab-item" v-if="tabIndex==1">
<div class="course-brief" v-html="course_info.brief"></div>
</div>
<div class="tab-item" v-if="tabIndex==2">
<div class="tab-item-title">
<p class="chapter">课程章节</p>
<p class="chapter-length">共{{course_chapters.length}}章 {{course_info.sections}}个课时</p>
</div>
<div class="chapter-item" v-for="chapter in course_chapters" :key="chapter.name">
<p class="chapter-title"><img src="@/assets/img/enum.svg" alt="">第{{chapter.chapter}}章·{{chapter.name}}
</p>
<ul class="section-list">
<li class="section-item" v-for="section in chapter.coursesections" :key="section.name">
<p class="name"><span class="index">{{chapter.chapter}}-{{section.orders}}</span>
{{section.name}}<span class="free" v-if="section.free_trail">免费</span></p>
<p class="time">{{section.duration}} <img src="@/assets/img/chapter-player.svg"></p>
<button class="try" v-if="section.free_trail">立即试学</button>
<button class="try" v-else>立即购买</button>
</li>
</ul>
</div>
</div>
<div class="tab-item" v-if="tabIndex==3">
用户评论
</div>
<div class="tab-item" v-if="tabIndex==4">
常见问题
</div>
</div>
<div class="course-side">
<div class="teacher-info">
<h4 class="side-title"><span>授课老师</span></h4>
<div class="teacher-content">
<div class="cont1">
<img :src="course_info.teacher.image">
<div class="name">
<p class="teacher-name">{{course_info.teacher.name}}
{{course_info.teacher.title}}</p>
<p class="teacher-title">{{course_info.teacher.signature}}</p>
</div>
</div>
<p class="narrative">{{course_info.teacher.brief}}</p>
</div>
</div>
</div>
</div>
</div>
<Footer/>
</div>
</template>
<script>
import Header from "@/components/Header"
import Footer from "@/components/Footer"
// 加载组件
import {videoPlayer} from 'vue-video-player';
export default {
name: "Detail",
data() {
return {
end_time_info:{
day:' ',
hour:' ',
minute:' ',
second:' ',
surplus_total_secondse:-2,
surplus_total_secondse_start:'',
timmer_setinterval:1 // 相当于一个第一次执行的开关,或者他可以变为间隔值,懒得再度封装了
},
tabIndex: 2, // 当前选项卡显示的下标
course_id: 0, // 当前课程信息的ID
course_info: {
teacher: {},
}, // 课程信息
course_chapters: [], // 课程的章节课时列表
playerOptions: this.$settings.playerOptions
}
},
computed: {
},
created() {
// 验证地址栏的id是否合法
this.get_course_id();
// 拿到course的data
this.get_course_data();
this.get_chapter();
},
methods: {
onPlayerPlay() {
// 当视频播放时,执行的方法
},
onPlayerPause() {
// 当视频暂停播放时,执行的方法
},
get_course_id() {
// 获取地址栏上面的课程ID
this.course_id = this.$route.params.pk;
if (this.course_id < 1) {
let _this = this;
_this.$alert("对不起,当前视频不存在!", "警告", {
callback() {
_this.$router.go(-1);
}
});
}
},
get_course_data() {
// ajax请求课程信息
this.$axios.get(`${this.$settings.base_url}/course/${this.course_id}/`).then(response => {
// window.console.log(response.data);
this.course_info = response.data;
// 剩余总共秒数
if (this.course_info.active_time){
this.end_time_info.surplus_total_secondse = this.get_seconds(this.course_info.active_time);
// this.end_time_info.surplus_total_secondse = this.get_seconds("2019-12-18 12:58:30");
this.timer();
// 统计剩余时间
this.end_time_info.timmer_setinterval = window.setInterval(this.timer, 1000);
}
}).catch(() => {
this.$message({
message: "对不起,访问页面出错!请联系客服工作人员!"
});
})
},
get_chapter() {
// 获取当前课程对应的章节课时信息
// http://127.0.0.1:8000/course/chapters/?course=(pk)
this.$axios.get(`${this.$settings.base_url}/course/chapters/`, {
params: {
"course": this.course_id,
}
}).then(response => {
this.course_chapters = response.data;
}).catch(error => {
window.console.log(error.response);
})
},
// add_cart(course_id) {
// // 添加商品到购物车
// // 验证用户登录状态,如果登录了则可以添加商品到购物车,如果没有登录则跳转到登录界面,登录完成以后,才能添加商品到购物车
// let token = localStorage.token || sessionStorage.token;
// if (!token) {
// this.$confirm("对不起,您尚未登录,请登录以后再进行购物车").then(() => {
// this.$router.push("/login/");
// });
// return false; // 阻止代码往下执行
// }
//
// // 添加商品到购物车,因为购物车接口必须用户是登录的,所以我们要在请求头中设置 jwttoken
// this.$axios.post(`${this.$settings.Host}/cart/`, {
// "course_id": course_id,
// }, {
// headers: {
// "Authorization": "jwt " + token,
// }
// }).then(response => {
// this.$message({
// message: response.data.message,
// });
// // 购物车中的商品数量
// let total = response.data.total;
// this.$store.commit("change_total", total)
// }).catch(error => {
// this.$message({
// message: error.response.data
// })
// })
// },
// 事件计数器
get_seconds(active_time){
let timeDate = active_time;
let end_time = new Date(timeDate);
let end_time_seconds = Math.floor(end_time.getTime()/1000);
let now = new Date();
let now_seconds = Math.floor(now.getTime()/1000);
let surplus_seconds = end_time_seconds - now_seconds;
return surplus_seconds
},
timer() {
// 为了让用户看到0的时候显示1s,其实在surplus_total_secondse=-1期间用户已经看不到优惠价格了
if (this.end_time_info.surplus_total_secondse < -1){
console.log('我被执行了');
clearInterval(this.end_time_info.timmer_setinterval);
// this.end_time_info.timmer_setinterval = ""
return
}
//开局第一次就显示剩余秒数.因为setinterval 会等一个间隔s才执行
if (this.end_time_info.surplus_total_secondse_start){
this.end_time_info.surplus_total_secondse = this.end_time_info.surplus_total_secondse_start+ this.end_time_info.surplus_total_secondse
}
// 第二次就进不来了,当执行到最后的时候会自己-1也就回到了正常s数。
this.end_time_info.surplus_total_secondse_start = '';
// 为什么可以等于0就是为了显示效果为了让用户看到0,实则优惠价格就在此刻已经消失。
if(this.end_time_info.surplus_total_secondse>=0){
let surplus_secondse = this.end_time_info.surplus_total_secondse;
//包含的天数
let day = Math.floor(surplus_secondse / (3600 * 24));
// console.log(d,'天数')
let s = surplus_secondse - day * 3600 * 24;
//计算包含的小时数
let hour = Math.floor(s / 3600);
s -= hour * 3600; //剩下的秒数
//计算包含的分钟数
let minute = Math.floor(s / 60);
//计算剩下的秒
let second = s - minute * 60;
this.end_time_info.day = day < 10 ? '0' + day : day;
this.end_time_info.hour = hour < 10 ? '0' + hour : hour;
this.end_time_info.minute = minute < 10 ? '0' + minute : minute;
this.end_time_info.second = second < 10 ? '0' + second : second; // 0s的时候代表所有的秒数走完了 显示的非常快0是看不到的
console.log(this.end_time_info.second);
// this.end_time_info.surplus_total_secondse -= 1;
}
this.end_time_info.surplus_total_secondse -= 1;
console.log(this.end_time_info.surplus_total_secondse);
},
},
components: {
Header,
Footer,
videoPlayer, // 注册组件
}
}
</script>
<style scoped>
.main {
background: #fff;
padding-top: 30px;
}
.course-info {
width: 1200px;
margin: 0 auto;
overflow: hidden;
}
.wrap-left {
float: left;
width: 690px;
height: 388px;
background-color: #000;
}
.wrap-right {
float: left;
position: relative;
height: 388px;
}
.course-name {
font-size: 20px;
color: #333;
padding: 10px 23px;
letter-spacing: .45px;
}
.data {
padding-left: 23px;
padding-right: 23px;
padding-bottom: 16px;
font-size: 14px;
color: #9b9b9b;
}
.sale-time {
width: 464px;
background: #fa6240;
font-size: 14px;
color: #4a4a4a;
padding: 10px 23px;
overflow: hidden;
}
.sale-type {
font-size: 16px;
color: #fff;
letter-spacing: .36px;
float: left;
}
.sale-time .expire {
font-size: 14px;
color: #fff;
float: right;
}
.sale-time .expire .second {
width: 22px;
display: inline-block;
background: #fafafa;
color: #5e5e5e;
/*padding: 3px 0 ;*/
line-height: 30px; /* 行高控制在一个元素内垂直居中*/
text-align: center;
height: 30px;
vertical-align: middle;
/*width: 15px;*/
}
.course-price {
background: #fff;
font-size: 14px;
color: #4a4a4a;
padding: 5px 23px;
}
.discount {
font-size: 26px;
color: #fa6240;
margin-left: 10px;
display: inline-block;
margin-bottom: -5px;
}
.original {
font-size: 14px;
color: #9b9b9b;
margin-left: 10px;
text-decoration: line-through;
}
.buy {
width: 464px;
padding: 0px 23px;
position: absolute;
left: 0;
bottom: 20px;
overflow: hidden;
}
.buy .buy-btn {
float: left;
}
.buy .buy-now {
width: 125px;
height: 40px;
border: 0;
background: #ffc210;
border-radius: 4px;
color: #fff;
cursor: pointer;
margin-right: 15px;
outline: none;
}
.buy .free {
width: 125px;
height: 40px;
border-radius: 4px;
cursor: pointer;
margin-right: 15px;
background: #fff;
color: #ffc210;
border: 1px solid #ffc210;
}
.add-cart {
float: right;
font-size: 14px;
color: #ffc210;
text-align: center;
cursor: pointer;
margin-top: 10px;
}
.add-cart img {
width: 20px;
height: 18px;
margin-right: 7px;
vertical-align: middle;
}
.course-tab {
width: 100%;
background: #fff;
margin-bottom: 30px;
box-shadow: 0 2px 4px 0 #f0f0f0;
}
.course-tab .tab-list {
width: 1200px;
margin: auto;
color: #4a4a4a;
overflow: hidden;
}
.tab-list li {
float: left;
margin-right: 15px;
padding: 26px 20px 16px;
font-size: 17px;
cursor: pointer;
}
.tab-list .active {
color: #ffc210;
border-bottom: 2px solid #ffc210;
}
.tab-list .free {
color: #fb7c55;
}
.course-content {
width: 1200px;
margin: 0 auto;
background: #FAFAFA;
overflow: hidden;
padding-bottom: 40px;
}
.course-tab-list {
width: 880px;
height: auto;
padding: 20px;
background: #fff;
float: left;
box-sizing: border-box;
overflow: hidden;
position: relative;
box-shadow: 0 2px 4px 0 #f0f0f0;
}
.tab-item {
width: 880px;
background: #fff;
padding-bottom: 20px;
box-shadow: 0 2px 4px 0 #f0f0f0;
}
.tab-item-title {
justify-content: space-between;
padding: 25px 20px 11px;
border-radius: 4px;
margin-bottom: 20px;
border-bottom: 1px solid #333;
border-bottom-color: rgba(51, 51, 51, .05);
overflow: hidden;
}
.chapter {
font-size: 17px;
color: #4a4a4a;
float: left;
}
.chapter-length {
float: right;
font-size: 14px;
color: #9b9b9b;
letter-spacing: .19px;
}
.chapter-title {
font-size: 16px;
color: #4a4a4a;
letter-spacing: .26px;
padding: 12px;
background: #eee;
border-radius: 2px;
display: -ms-flexbox;
display: flex;
-ms-flex-align: center;
align-items: center;
}
.chapter-title img {
width: 18px;
height: 18px;
margin-right: 7px;
vertical-align: middle;
}
.section-list {
padding: 0 20px;
}
.section-list .section-item {
padding: 15px 20px 15px 36px;
cursor: pointer;
justify-content: space-between;
position: relative;
overflow: hidden;
}
.section-item .name {
font-size: 14px;
color: #666;
float: left;
}
.section-item .index {
margin-right: 5px;
}
.section-item .free {
font-size: 12px;
color: #fff;
letter-spacing: .19px;
background: #ffc210;
border-radius: 100px;
padding: 1px 9px;
margin-left: 10px;
}
.section-item .time {
font-size: 14px;
color: #666;
letter-spacing: .23px;
opacity: 1;
transition: all .15s ease-in-out;
float: right;
}
.section-item .time img {
width: 18px;
height: 18px;
margin-left: 15px;
vertical-align: text-bottom;
}
.section-item .try {
width: 86px;
height: 28px;
background: #ffc210;
border-radius: 4px;
font-size: 14px;
color: #fff;
position: absolute;
right: 20px;
top: 10px;
opacity: 0;
transition: all .2s ease-in-out;
cursor: pointer;
outline: none;
border: none;
}
.section-item:hover {
background: #fcf7ef;
box-shadow: 0 0 0 0 #f3f3f3;
}
.section-item:hover .name {
color: #333;
}
.section-item:hover .try {
opacity: 1;
}
.course-side {
width: 300px;
height: auto;
margin-left: 20px;
float: right;
}
.teacher-info {
background: #fff;
margin-bottom: 20px;
box-shadow: 0 2px 4px 0 #f0f0f0;
}
.side-title {
font-weight: normal;
font-size: 17px;
color: #4a4a4a;
padding: 18px 14px;
border-bottom: 1px solid #333;
border-bottom-color: rgba(51, 51, 51, .05);
}
.side-title span {
display: inline-block;
border-left: 2px solid #ffc210;
padding-left: 12px;
}
.teacher-content {
padding: 30px 20px;
box-sizing: border-box;
}
.teacher-content .cont1 {
margin-bottom: 12px;
overflow: hidden;
}
.teacher-content .cont1 img {
width: 54px;
height: 54px;
margin-right: 12px;
float: left;
}
.teacher-content .cont1 .name {
float: right;
}
.teacher-content .cont1 .teacher-name {
width: 188px;
font-size: 16px;
color: #4a4a4a;
padding-bottom: 4px;
}
.teacher-content .cont1 .teacher-title {
width: 188px;
font-size: 13px;
color: #9b9b9b;
white-space: nowrap;
}
.teacher-content .narrative {
font-size: 14px;
color: #666;
line-height: 24px;
}
</style>
3 后端代码
2 course/models.py
from django.db import models
from utils.model import BaseModel
"""
一个分类有多个课程以及自己的分类名字
一个老师讲了多个课程
一 多
分类 课程
老师 课程
课程 章节
章节 课时
课程:分类(1-n)、老师(1-n)
分类:
老师:
章节:课程(1-n)
课时:章节(1-n)
"""
class CourseCategory(BaseModel):
"""分类"""
name = models.CharField(max_length=64, unique=True, verbose_name="分类名称")
class Meta:
db_table = "luffy_course_category"
verbose_name = "分类"
verbose_name_plural = verbose_name
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="courses", 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=0, verbose_name="难度等级")
pub_date = models.DateField(verbose_name="发布日期", auto_now_add=True)
period = models.IntegerField(verbose_name="建议学习周期(day)", default=7)
attachment_path = models.FileField(upload_to="attachment", 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.SET_NULL, db_constraint=False, null=True, blank=True,
verbose_name="课程分类")
students = models.IntegerField(verbose_name="学习人数", default=0)
sections = models.IntegerField(verbose_name="总课时数量", default=0)
pub_sections = models.IntegerField(verbose_name="课时更新数量", default=0)
price = models.DecimalField(max_digits=6, decimal_places=2, verbose_name="课程原价", default=0)
real_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="授课老师")
# 连表查询
@property
def section_list(self):
temp_section_list = []
for chapter in self.coursechapters.all():
for section in chapter.coursesections.all():
# 最多需要4条数据
if len(temp_section_list) >= 4:
return temp_section_list
temp_section_list.append({
'free_trail': section.free_trail,
'name': section.name
})
return temp_section_list
class Meta:
db_table = "luffy_course"
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 = "luffy_course_chapter"
verbose_name = "章节"
verbose_name_plural = verbose_name
def __str__(self):
return "%s:(第%s章)%s" % (self.course, self.chapter, self.name)
class CourseSection(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 = "luffy_course_Section"
verbose_name = "课时"
verbose_name_plural = verbose_name
def __str__(self):
return "%s-%s" % (self.chapter, self.name)
class Teacher(BaseModel):
"""导师"""
role_choices = (
(0, '讲师'),
(1, '导师'),
(2, '班主任'),
)
name = models.CharField(max_length=32, verbose_name="导师名")
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, verbose_name="导师封面")
brief = models.TextField(max_length=1024, verbose_name="导师描述")
class Meta:
db_table = "luffy_teacher"
verbose_name = "导师"
verbose_name_plural = verbose_name
def __str__(self):
return "%s" % self.name
1 course/serializers.py
class CourseRetriveModelSerializer(ModelSerializer):
teacher = TeacherModelSerializer()
# get 要和要用serializerMethodField来做。
active_time = serializers.SerializerMethodField(read_only=True)
class Meta:
model = models.Course
fields = ('id',
'name',
'students',
'sections',
'pub_sections',
'level_name',
# 'course_type', 待定
'discount_type',
'active_time',# 写个死的
'real_price',
'price',
'teacher',
'brief',
)
extra_kwargs = {
'active_time': {
}}
def get_name(self,obj):
print(obj)
return obj
def get_active_time(self, obj):
res = obj.active_time.strftime("%Y-%m-%d %H:%M:%S")
return res
class Coursesectionserializer(ModelSerializer):
class Meta:
model = models.CourseSection
fields =('name','free_trail','orders','section_link')
class CourseChapterserializer(ModelSerializer):
coursesections = Coursesectionserializer(many=True) # 要是查的是很多条,不要忘记协商many=True
class Meta:
model = models.CourseChapter
fields = ('id','name','chapter','coursesections')
2 course/views.py
# 课程细节页面的课程信息
class CouseRetrieveAPIView(RetrieveAPIView):
queryset = models.Course.objects.filter(is_delete=False,is_show=True)
serializer_class = serializers.CourseRetriveModelSerializer
# 章节详情
class ChapterListAPIView(ListAPIView):
queryset = models.CourseChapter.objects.filter(is_delete=False,is_show=True)
serializer_class = serializers.CourseChapterserializer
filter_backends = [OrderingFilter,DjangoFilterBackend]
filter_fields = ('course',)
3 course/urls
urlpatterns = [
path('category/',views.CategoryListAPIViws.as_view()),
path('',views.CourseListAPIView.as_view()),
re_path('^(?P<pk>\d*)/$',views.CouseRetrieveAPIView.as_view()),#re_path应该放到最下面否者找不到
path('chapters/',views.ChapterListAPIView.as_view()),
path('test/',views.Test.as_view()),
]