富文本即具备丰富样式格式的文本。在运营后台,运营人员需要录入课程的相关描述,可以是包含了HTML语法格式的字符串。为了快速简单的让用户能够在页面中编辑带html格式的文本,引入富文本编辑器。
pip install django-ckeditor
在INSTALLED_APPS中添加
INSTALLED_APPS = [
...
'ckeditor', # 富文本编辑器
'ckeditor_uploader', # 富文本编辑器上传图片模块
...
]
在settings/dev.py中添加
# 富文本编辑器ckeditor配置
CKEDITOR_CONFIGS = {
'default': {
'toolbar': 'full', # 工具条功能,full表示全部,Basic表示基本功能,功能少很多,还有个Custom自定义功能选项
'height': 300, # 编辑器高度
# 'width': 300, # 编辑器宽
},
}
CKEDITOR_UPLOAD_PATH = '' # 上传图片保存路径,留空则调用django的文件上传功能
#自定义
CKEDITOR_CONFIGS = {
'default': {
'toolbar': 'Custom',
'toolbar_Custom': [
['Bold', 'Italic', 'Underline','Image'], #通过浏览器f12来查看每个功能的标签,就看到了类值cke_button_工具名称[注意使用驼峰式来写]
['NumberedList', 'BulletedList', '-', 'Outdent', 'Indent', '-', 'JustifyLeft', 'JustifyCenter', 'JustifyRight', 'JustifyBlock'],
['Link', 'Unlink'],
['RemoveFormat', 'Source']
]
}
}
在总路由中添加
path(r'^ckeditor/', include('ckeditor_uploader.urls')),
ckeditor提供了两种类型的Django模型类字段
ckeditor.fields.RichTextField
不支持上传文件的富文本字段ckeditor_uploader.fields.RichTextUploadingField
支持上传文件的富文本字段修改course/models.py里面的字段信息,记得要重新数据迁移
from ckeditor_uploader.fields import RichTextUploadingField
class Course(models.Model):
"""
专题课程
"""
...
brief = RichTextUploadingField(max_length=2048, verbose_name="课程概述", null=True, blank=True)
Detail.vue
<template>
<div class="detail">
<Vheader/>
<div class="main">
<div class="course-info">
<div class="wrap-left">
div>
<div class="wrap-right">
<h3 class="course-name">flaskh3>
<p class="data">111人在学 课程总时长:111课时/12小时 难度:p>
<div class="sale-time">
<p class="sale-type">限时免费p>
<p class="expire">距离结束:仅剩 01天 04小时 33分 <span class="second">08span> 秒p>
div>
<p class="course-price">
<span>活动价span>
<span class="discount">¥0.00span>
<span class="original">¥1111span>
p>
<div class="buy">
<div class="buy-btn">
<button class="buy-now">立即购买button>
<button class="free">免费试学button>
div>
<div class="add-cart"><img src="/static/img/cart-yellow.svg" alt="">加入购物车div>
div>
div>
div>
<div class="course-tab">
<ul class="tab-list">
<li :class="tabIndex==1?'active':''" @click="tabIndex=1">详情介绍li>
<li :class="tabIndex==2?'active':''" @click="tabIndex=2">课程章节 <span :class="tabIndex!=2?'free':''">(试学)span>li>
<li :class="tabIndex==3?'active':''" @click="tabIndex=3">用户评论 (42)li>
<li :class="tabIndex==4?'active':''" @click="tabIndex=4">常见问题li>
ul>
div>
<div class="course-content">
<div class="course-tab-list">
<div class="tab-item" v-if="tabIndex==1">
<div class="course-brief" v-html="">div>
div>
<div class="tab-item" v-if="tabIndex==2">
<div class="tab-item-title">
<p class="chapter">课程章节p>
<p class="chapter-length">共11章 147个课时p>
div>
<div class="chapter-item">
<p class="chapter-title"><img src="/static/img/1.png" alt="">第1章·Linux硬件基础p>
<ul class="lesson-list">
<li class="lesson-item">
<p class="name"><span class="index">1-1span> 课程介绍-学习流程<span class="free">免费span>p>
<p class="time">07:30 <img src="/static/img/chapter-player.svg">p>
<button class="try">立即试学button>
li>
<li class="lesson-item">
<p class="name"><span class="index">1-2span> 服务器硬件-详解<span class="free">免费span>p>
<p class="time">07:30 <img src="/static/img/chapter-player.svg">p>
<button class="try">立即试学button>
li>
ul>
div>
<div class="chapter-item">
<p class="chapter-title"><img src="/static/img/1.png" alt="">第2章·Linux发展过程p>
<ul class="lesson-list">
<li class="lesson-item">
<p class="name"><span class="index">2-1span> 操作系统组成-Linux发展过程p>
<p class="time">07:30 <img src="/static/img/chapter-player.svg">p>
<button class="try">立即购买button>
li>
<li class="lesson-item">
<p class="name"><span class="index">2-2span> 自由软件-GNU-GPL核心讲解p>
<p class="time">07:30 <img src="/static/img/chapter-player.svg">p>
<button class="try">立即购买button>
li>
ul>
div>
div>
<div class="tab-item" v-if="tabIndex==3">
用户评论
div>
<div class="tab-item" v-if="tabIndex==4">
常见问题
div>
div>
<div class="course-side">
<div class="teacher-info">
<h4 class="side-title"><span>授课老师span>h4>
<div class="teacher-content">
<div class="cont1">
<img src="">
<div class="name">
<p class="teacher-name">xxxp>
<p class="teacher-title">ssssp>
div>
div>
<p class="narrative" >kkkkp>
div>
div>
div>
div>
div>
<Footer/>
div>
template>
<script>
import Vheader from "./common/Vheader"
import Footer from "./common/Footer"
export default {
name: "Detail",
data(){
return {
tabIndex:1,
}
},
created(){
},
methods: {
},
components:{
Vheader,
Footer,
}
}
script>
<style scoped>
.main{
background: #fff;
padding-top: 30px;
}
.course-info{
width: 1200px;
margin: 0 auto;
overflow: hidden;
}
.wrap-left{
float: left;
width: 690px;
height: 388px;
background-color: #000;
}
.wrap-right{
float: left;
position: relative;
height: 388px;
}
.course-name{
font-size: 20px;
color: #333;
padding: 10px 23px;
letter-spacing: .45px;
}
.data{
padding-left: 23px;
padding-right: 23px;
padding-bottom: 16px;
font-size: 14px;
color: #9b9b9b;
}
.sale-time{
width: 464px;
background: #fa6240;
font-size: 14px;
color: #4a4a4a;
padding: 10px 23px;
overflow: hidden;
}
.sale-type {
font-size: 16px;
color: #fff;
letter-spacing: .36px;
float: left;
}
.sale-time .expire{
font-size: 14px;
color: #fff;
float: right;
}
.sale-time .expire .second{
width: 24px;
display: inline-block;
background: #fafafa;
color: #5e5e5e;
padding: 6px 0;
text-align: center;
}
.course-price{
background: #fff;
font-size: 14px;
color: #4a4a4a;
padding: 5px 23px;
}
.discount{
font-size: 26px;
color: #fa6240;
margin-left: 10px;
display: inline-block;
margin-bottom: -5px;
}
.original{
font-size: 14px;
color: #9b9b9b;
margin-left: 10px;
text-decoration: line-through;
}
.buy{
width: 464px;
padding: 0px 23px;
position: absolute;
left: 0;
bottom: 20px;
overflow: hidden;
}
.buy .buy-btn{
float: left;
}
.buy .buy-now{
width: 125px;
height: 40px;
border: 0;
background: #ffc210;
border-radius: 4px;
color: #fff;
cursor: pointer;
margin-right: 15px;
outline: none;
}
.buy .free{
width: 125px;
height: 40px;
border-radius: 4px;
cursor: pointer;
margin-right: 15px;
background: #fff;
color: #ffc210;
border: 1px solid #ffc210;
}
.add-cart{
float: right;
font-size: 14px;
color: #ffc210;
text-align: center;
cursor: pointer;
margin-top: 10px;
}
.add-cart img{
width: 20px;
height: 18px;
margin-right: 7px;
vertical-align: middle;
}
.course-tab{
width: 100%;
background: #fff;
margin-bottom: 30px;
box-shadow: 0 2px 4px 0 #f0f0f0;
}
.course-tab .tab-list{
width: 1200px;
margin: auto;
color: #4a4a4a;
overflow: hidden;
}
.tab-list li{
float: left;
margin-right: 15px;
padding: 26px 20px 16px;
font-size: 17px;
cursor: pointer;
}
.tab-list .active{
color: #ffc210;
border-bottom: 2px solid #ffc210;
}
.tab-list .free{
color: #fb7c55;
}
.course-content{
width: 1200px;
margin: 0 auto;
background: #FAFAFA;
overflow: hidden;
padding-bottom: 40px;
}
.course-tab-list{
width: 880px;
height: auto;
padding: 20px;
background: #fff;
float: left;
box-sizing: border-box;
overflow: hidden;
position: relative;
box-shadow: 0 2px 4px 0 #f0f0f0;
}
.tab-item{
width: 880px;
background: #fff;
padding-bottom: 20px;
box-shadow: 0 2px 4px 0 #f0f0f0;
}
.tab-item-title{
justify-content: space-between;
padding: 25px 20px 11px;
border-radius: 4px;
margin-bottom: 20px;
border-bottom: 1px solid #333;
border-bottom-color: rgba(51,51,51,.05);
overflow: hidden;
}
.chapter{
font-size: 17px;
color: #4a4a4a;
float: left;
}
.chapter-length{
float: right;
font-size: 14px;
color: #9b9b9b;
letter-spacing: .19px;
}
.chapter-title{
font-size: 16px;
color: #4a4a4a;
letter-spacing: .26px;
padding: 12px;
background: #eee;
border-radius: 2px;
display: -ms-flexbox;
display: flex;
-ms-flex-align: center;
align-items: center;
}
.chapter-title img{
width: 18px;
height: 18px;
margin-right: 7px;
vertical-align: middle;
}
.lesson-list{
padding:0 20px;
}
.lesson-list .lesson-item{
padding: 15px 20px 15px 36px;
cursor: pointer;
justify-content: space-between;
position: relative;
overflow: hidden;
}
.lesson-item .name{
font-size: 14px;
color: #666;
float: left;
}
.lesson-item .index{
margin-right: 5px;
}
.lesson-item .free{
font-size: 12px;
color: #fff;
letter-spacing: .19px;
background: #ffc210;
border-radius: 100px;
padding: 1px 9px;
margin-left: 10px;
}
.lesson-item .time{
font-size: 14px;
color: #666;
letter-spacing: .23px;
opacity: 1;
transition: all .15s ease-in-out;
float: right;
}
.lesson-item .time img{
width: 18px;
height: 18px;
margin-left: 15px;
vertical-align: text-bottom;
}
.lesson-item .try{
width: 86px;
height: 28px;
background: #ffc210;
border-radius: 4px;
font-size: 14px;
color: #fff;
position: absolute;
right: 20px;
top: 10px;
opacity: 0;
transition: all .2s ease-in-out;
cursor: pointer;
outline: none;
border: none;
}
.lesson-item:hover{
background: #fcf7ef;
box-shadow: 0 0 0 0 #f3f3f3;
}
.lesson-item:hover .name{
color: #333;
}
.lesson-item:hover .try{
opacity: 1;
}
.course-side{
width: 300px;
height: auto;
margin-left: 20px;
float: right;
}
.teacher-info{
background: #fff;
margin-bottom: 20px;
box-shadow: 0 2px 4px 0 #f0f0f0;
}
.side-title{
font-weight: normal;
font-size: 17px;
color: #4a4a4a;
padding: 18px 14px;
border-bottom: 1px solid #333;
border-bottom-color: rgba(51,51,51,.05);
}
.side-title span{
display: inline-block;
border-left: 2px solid #ffc210;
padding-left: 12px;
}
.teacher-content{
padding: 30px 20px;
box-sizing: border-box;
}
.teacher-content .cont1{
margin-bottom: 12px;
overflow: hidden;
}
.teacher-content .cont1 img{
width: 54px;
height: 54px;
margin-right: 12px;
float: left;
}
.teacher-content .cont1 .name{
float: right;
}
.teacher-content .cont1 .teacher-name{
width: 188px;
font-size: 16px;
color: #4a4a4a;
padding-bottom: 4px;
}
.teacher-content .cont1 .teacher-title{
width: 188px;
font-size: 13px;
color: #9b9b9b;
white-space: nowrap;
}
.teacher-content .narrative{
font-size: 14px;
color: #666;
line-height: 24px;
}
style>
index.js
import Detail from '@/components/Detail'
...
{
path: '/course/detail/:id/',
component: Detail
},
Course.vue
...
<h3><router-link :to="'/course/detail/'+course.id+'/'">{{ course.name }}router-link> <span><img src="/static/img/avatar1.svg" alt="">{{course.students}}人已加入学习span>h3>
...
因为接下来的组件中使用了vue-video视频播放组件,所以我们需要先预安装。
安装依赖
npm install vue-video-player --save
在main.js中注册加载组件
require('video.js/dist/video-js.css');
require('vue-video-player/src/custom-theme.css');
import VideoPlayer from 'vue-video-player'
Vue.use(VideoPlayer);
总Detail.vue组件
<template>
<div class="detail">
<Vheader/>
<div class="main">
<div class="course-info">
<div class="wrap-left">
<videoPlayer
class="video-player vjs-custom-skin"
ref="videoPlayer"
:playsinline="true"
:options="playerOptions"
@play="onPlayerPlay($event)"
@pause="onPlayerPause($event)">
videoPlayer>
div>
<div class="wrap-right">
<h3 class="course-name">{{course_data.name}}h3>
<p class="data">{{course_data.students}}人在学 课程总时长:{{course_data.lessons}}课时 难度:{{course_data.level_name}}p>
<div class="sale-time">
<p class="sale-type">限时免费p>
<p class="expire">距离结束:仅剩 01天 04小时 33分 <span class="second">08span> 秒p>
div>
<p class="course-price">
<span>活动价span>
<span class="discount">¥0.00span>
<span class="original">¥{{course_data.price}}span>
p>
<div class="buy">
<div class="buy-btn">
<button class="buy-now">立即购买button>
<button class="free">免费试学button>
div>
<div class="add-cart"><img src="/static/img/cart-yellow.svg" alt="">加入购物车div>
div>
div>
div>
<div class="course-tab">
<ul class="tab-list">
<li :class="tabIndex==1?'active':''" @click="tabIndex=1">详情介绍li>
<li :class="tabIndex==2?'active':''" @click="tabIndex=2">课程章节 <span :class="tabIndex!=2?'free':''">(试学)span>li>
<li :class="tabIndex==3?'active':''" @click="tabIndex=3">用户评论 (42)li>
<li :class="tabIndex==4?'active':''" @click="tabIndex=4">常见问题li>
ul>
div>
<div class="course-content">
<div class="course-tab-list">
<div class="tab-item" v-if="tabIndex==1">
<div class="course-brief" v-html="course_data.new_brief">div>
div>
<div class="tab-item" v-if="tabIndex==2">
<div class="tab-item-title">
<p class="chapter">课程章节p>
<p class="chapter-length">共{{chapter_data.length}}章p>
div>
<div class="chapter-item" v-for="(chapter, chapterindex) in chapter_data">
<p class="chapter-title"><img src="/static/img/1.png" alt="">第{{chapter.chapter}}章·{{chapter.name}}p>
<ul class="lesson-list">
<p class="name"><span class="index">{{chapter.chapter}}-{{lesson.lesson}}span> 课程介绍-{{lesson.name}}<span v-show="lesson.free_trail" class="free">免费span>p>
<p class="time">{{lesson.duration}} <img src="/static/img/chapter-player.svg">p>
<button class="try" v-if="lesson.free_trail">立即试学button>
<button class="try" v-else>立即购买button>
li>
ul>
div>
<div class="chapter-item">
<p class="chapter-title"><img src="/static/img/1.png" alt="">第2章·Linux发展过程p>
<ul class="lesson-list">
<li class="lesson-item">
<p class="name"><span class="index">2-1span> 操作系统组成-Linux发展过程p>
<p class="time">07:30 <img src="/static/img/chapter-player.svg">p>
<button class="try">立即购买button>
li>
<li class="lesson-item">
<p class="name"><span class="index">2-2span> 自由软件-GNU-GPL核心讲解p>
<p class="time">07:30 <img src="/static/img/chapter-player.svg">p>
<button class="try">立即购买button>
li>
ul>
div>
div>
<div class="tab-item" v-if="tabIndex==3">
用户评论
div>
<div class="tab-item" v-if="tabIndex==4">
常见问题
div>
div>
<div class="course-side">
<div class="teacher-info">
<h4 class="side-title"><span>授课老师span>h4>
<div class="teacher-content">
<div class="cont1">
<img src="">
<div class="name">
<p class="teacher-name">{{course_data.teacher.name}}p>
<p class="teacher-title">{{course_data.teacher.title}}p>
div>
div>
<p class="narrative" >{{course_data.teacher.signature}}p>
div>
div>
div>
div>
div>
<Footer/>
div>
template>
<script>
import Vheader from "./common/Vheader"
import Footer from "./common/Footer"
import {videoPlayer} from 'vue-video-player';
export default {
name: "Detail",
data(){
return {
tabIndex:1,
course_id: 0,
course_data: {
teacher:{}
},
chapter_data: {},
playerOptions: {
playbackRates: [0.7, 1.0, 1.5, 2.0], // 播放速度
autoplay: false, //如果true,则自动播放
muted: false, // 默认情况下将会消除任何音频。
loop: false, // 循环播放
preload: 'auto', // 建议浏览器在
language: 'zh-CN',
aspectRatio: '16:9', // 将播放器置于流畅模式,并在计算播放器的动态大小时使用该值。值应该代表一个比例 - 用冒号分隔的两个数字(例如"16:9"或"4:3")
fluid: true, // 当true时,Video.js player将拥有流体大小。换句话说,它将按比例缩放以适应其容器。
sources: [{ // 播放资源和资源格式
type: "video/mp4",
src: "", //你的视频地址(必填)
}],
poster: "", //视频封面图
width: document.documentElement.clientWidth, // 默认视频全屏时的最大宽度
notSupportedMessage: '此视频暂无法播放,请稍后再试', //允许覆盖Video.js无法播放媒体源时显示的默认信息。
}
}
},
created(){
this.get_course_id();
this.get_course_data();
this.get_chapter_data();
},
methods: {
get_course_id(){
this.course_id = this.$route.params.id;
},
get_course_data(){
this.$axios.get(`${this.$settings.Host}/course/detail/${this.course_id}/`)
.then((res)=>{
this.course_data = res.data;
this.playerOptions.sources[0].src = res.data.course_video
this.playerOptions.poster = res.data.course_img
})
},
get_chapter_data(){
this.$axios.get(`${this.$settings.Host}/course/chapter/`,{
params:{
course:this.course_id,
}
}).then((res)=>{
console.log(res.data);
this.chapter_data = res.data
})
},
onPlayerPlay(e){
alert('开始播放')
},
onPlayerPause(e){
alert('暂停播放')
},
},
components:{
Vheader,
Footer,
videoPlayer,
}
}
script>
<style scoped>
.main{
background: #fff;
padding-top: 30px;
}
.course-info{
width: 1200px;
margin: 0 auto;
overflow: hidden;
}
.wrap-left{
float: left;
width: 690px;
height: 388px;
background-color: #000;
}
.wrap-right{
float: left;
position: relative;
height: 388px;
}
.course-name{
font-size: 20px;
color: #333;
padding: 10px 23px;
letter-spacing: .45px;
}
.data{
padding-left: 23px;
padding-right: 23px;
padding-bottom: 16px;
font-size: 14px;
color: #9b9b9b;
}
.sale-time{
width: 464px;
background: #fa6240;
font-size: 14px;
color: #4a4a4a;
padding: 10px 23px;
overflow: hidden;
}
.sale-type {
font-size: 16px;
color: #fff;
letter-spacing: .36px;
float: left;
}
.sale-time .expire{
font-size: 14px;
color: #fff;
float: right;
}
.sale-time .expire .second{
width: 24px;
display: inline-block;
background: #fafafa;
color: #5e5e5e;
padding: 6px 0;
text-align: center;
}
.course-price{
background: #fff;
font-size: 14px;
color: #4a4a4a;
padding: 5px 23px;
}
.discount{
font-size: 26px;
color: #fa6240;
margin-left: 10px;
display: inline-block;
margin-bottom: -5px;
}
.original{
font-size: 14px;
color: #9b9b9b;
margin-left: 10px;
text-decoration: line-through;
}
.buy{
width: 464px;
padding: 0px 23px;
position: absolute;
left: 0;
bottom: 20px;
overflow: hidden;
}
.buy .buy-btn{
float: left;
}
.buy .buy-now{
width: 125px;
height: 40px;
border: 0;
background: #ffc210;
border-radius: 4px;
color: #fff;
cursor: pointer;
margin-right: 15px;
outline: none;
}
.buy .free{
width: 125px;
height: 40px;
border-radius: 4px;
cursor: pointer;
margin-right: 15px;
background: #fff;
color: #ffc210;
border: 1px solid #ffc210;
}
.add-cart{
float: right;
font-size: 14px;
color: #ffc210;
text-align: center;
cursor: pointer;
margin-top: 10px;
}
.add-cart img{
width: 20px;
height: 18px;
margin-right: 7px;
vertical-align: middle;
}
.course-tab{
width: 100%;
background: #fff;
margin-bottom: 30px;
box-shadow: 0 2px 4px 0 #f0f0f0;
}
.course-tab .tab-list{
width: 1200px;
margin: auto;
color: #4a4a4a;
overflow: hidden;
}
.tab-list li{
float: left;
margin-right: 15px;
padding: 26px 20px 16px;
font-size: 17px;
cursor: pointer;
}
.tab-list .active{
color: #ffc210;
border-bottom: 2px solid #ffc210;
}
.tab-list .free{
color: #fb7c55;
}
.course-content{
width: 1200px;
margin: 0 auto;
background: #FAFAFA;
overflow: hidden;
padding-bottom: 40px;
}
.course-tab-list{
width: 880px;
height: auto;
padding: 20px;
background: #fff;
float: left;
box-sizing: border-box;
overflow: hidden;
position: relative;
box-shadow: 0 2px 4px 0 #f0f0f0;
}
.tab-item{
width: 880px;
background: #fff;
padding-bottom: 20px;
box-shadow: 0 2px 4px 0 #f0f0f0;
}
.tab-item-title{
justify-content: space-between;
padding: 25px 20px 11px;
border-radius: 4px;
margin-bottom: 20px;
border-bottom: 1px solid #333;
border-bottom-color: rgba(51,51,51,.05);
overflow: hidden;
}
.chapter{
font-size: 17px;
color: #4a4a4a;
float: left;
}
.chapter-length{
float: right;
font-size: 14px;
color: #9b9b9b;
letter-spacing: .19px;
}
.chapter-title{
font-size: 16px;
color: #4a4a4a;
letter-spacing: .26px;
padding: 12px;
background: #eee;
border-radius: 2px;
display: -ms-flexbox;
display: flex;
-ms-flex-align: center;
align-items: center;
}
.chapter-title img{
width: 18px;
height: 18px;
margin-right: 7px;
vertical-align: middle;
}
.lesson-list{
padding:0 20px;
}
.lesson-list .lesson-item{
padding: 15px 20px 15px 36px;
cursor: pointer;
justify-content: space-between;
position: relative;
overflow: hidden;
}
.lesson-item .name{
font-size: 14px;
color: #666;
float: left;
}
.lesson-item .index{
margin-right: 5px;
}
.lesson-item .free{
font-size: 12px;
color: #fff;
letter-spacing: .19px;
background: #ffc210;
border-radius: 100px;
padding: 1px 9px;
margin-left: 10px;
}
.lesson-item .time{
font-size: 14px;
color: #666;
letter-spacing: .23px;
opacity: 1;
transition: all .15s ease-in-out;
float: right;
}
.lesson-item .time img{
width: 18px;
height: 18px;
margin-left: 15px;
vertical-align: text-bottom;
}
.lesson-item .try{
width: 86px;
height: 28px;
background: #ffc210;
border-radius: 4px;
font-size: 14px;
color: #fff;
position: absolute;
right: 20px;
top: 10px;
opacity: 0;
transition: all .2s ease-in-out;
cursor: pointer;
outline: none;
border: none;
}
.lesson-item:hover{
background: #fcf7ef;
box-shadow: 0 0 0 0 #f3f3f3;
}
.lesson-item:hover .name{
color: #333;
}
.lesson-item:hover .try{
opacity: 1;
}
.course-side{
width: 300px;
height: auto;
margin-left: 20px;
float: right;
}
.teacher-info{
background: #fff;
margin-bottom: 20px;
box-shadow: 0 2px 4px 0 #f0f0f0;
}
.side-title{
font-weight: normal;
font-size: 17px;
color: #4a4a4a;
padding: 18px 14px;
border-bottom: 1px solid #333;
border-bottom-color: rgba(51,51,51,.05);
}
.side-title span{
display: inline-block;
border-left: 2px solid #ffc210;
padding-left: 12px;
}
.teacher-content{
padding: 30px 20px;
box-sizing: border-box;
}
.teacher-content .cont1{
margin-bottom: 12px;
overflow: hidden;
}
.teacher-content .cont1 img{
width: 54px;
height: 54px;
margin-right: 12px;
float: left;
}
.teacher-content .cont1 .name{
float: right;
}
.teacher-content .cont1 .teacher-name{
width: 188px;
font-size: 16px;
color: #4a4a4a;
padding-bottom: 4px;
}
.teacher-content .cont1 .teacher-title{
width: 188px;
font-size: 13px;
color: #9b9b9b;
white-space: nowrap;
}
.teacher-content .narrative{
font-size: 14px;
color: #666;
line-height: 24px;
}
style>
models.py
...
course_video = models.FileField(upload_to="video", verbose_name="封面video", blank=True, null=True, max_length=255)
...
数据库迁移指令
course/urls.py
from django.urls import path,re_path
from . import views
urlpatterns = [
path(r'categorys/', views.CategoryView.as_view()),
path(r'courses/', views.CourseView.as_view()),
re_path(r'courses/detail/(?P\d+)/' , views.CourseDetailView.as_view()),
]
views.py
...
from rest_framework.generics import ListAPIView,RetrieveAPIView
...
class CourseDetailView(RetrieveAPIView):
queryset = models.Course.objects.filter(is_deleted=False, is_show=True)
serializer_class = CourseDetailModelSerializer
class ChapterView(ListAPIView):
queryset = models.CourseChapter.objects.filter(is_deleted=False,is_show=True)
serializer_class = CourseChapterModelSerializer
filter_backends = [DjangoFilterBackend]
filter_fields = ('course',)
serializers.py
class CourseDetailModelSerializer(serializers.ModelSerializer):
# 序列化器嵌套
teacher = TeacherModelSerializer() # 将外键关联的属性指定为关联表的序列化器对象,就能拿到关联表序列化出来的所有数据,还需要在fields中指定一下,注意,名称必须和外键属性名称相同
class Meta:
model = models.Course
# fields = ["id","name","course_img","students","lessons","pub_lessons","price","teacher",'teacher_name'] #teacher外键属性默认拿的是id值
fields = ["id", "name", "course_img", "students", "lessons", "pub_lessons", "price", "teacher",
"level_name", "course_video",'new_brief'] # teacher外键属性默认拿的是id值
class CourseLessonModelSerializer(serializers.ModelSerializer):
class Meta:
model = models.CourseLesson
fields = ['name','section_link','duration','free_trail', 'lesson']
class CourseChapterModelSerializer(serializers.ModelSerializer):
coursesections = CourseLessonModelSerializer(many=True)
class Meta:
model = models.CourseChapter
fields = ['chapter', 'name', 'coursesections']
用postman测试
contains.py
...
SERVER_ADDR = 'http://www.lyapi.com:8001'
models.py
class Course(BaseModel):
"""
专题课程
"""
....
class Meta:
db_table = "ly_course"
verbose_name = "专题课程"
verbose_name_plural = "专题课程"
def __str__(self):
return "%s" % self.name
# 通过课程对象获取所有课程列表页中要展示的课时信息
def get_lessons(self):
chapters = self.coursechapters.all()
lession_list = []
for chapter in chapters:
lessons = chapter.coursesections.filter(is_show_list=True,is_show=True,is_deleted=False)
for lesson in lessons:
lession_list.append({
'name':lesson.name,
'free_trail':lesson.free_trail,
'lesson':lesson.lesson,
})
return lession_list[:4]
def level_name(self):
return self.get_level_display()
def new_brief(self):
data = self.brief
server_addr = contains.SERVER_ADDR
data = data.replace('src="/media',f'class="img_xx" src="{server_addr}/media')
return data