luffcc项目-08-课程详情页、CKEditor富文本编辑器、课程详情页面、后台接口

课程详情页

一、CKEditor富文本编辑器

富文本即具备丰富样式格式的文本。在运营后台,运营人员需要录入课程的相关描述,可以是包含了HTML语法格式的字符串。为了快速简单的让用户能够在页面中编辑带html格式的文本,引入富文本编辑器。

1. 安装
pip install django-ckeditor
2. 添加应用

在INSTALLED_APPS中添加

INSTALLED_APPS = [
    ...
    'ckeditor',  # 富文本编辑器
    'ckeditor_uploader',  # 富文本编辑器上传图片模块
    ...
]
3. 添加CKEditor设置

在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']
        ]
    }
}
4.添加ckeditor路由

在总路由中添加

path(r'^ckeditor/', include('ckeditor_uploader.urls')),
5.为模型类添加字段

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)
    

二、课程详情页显示

1.初始课程详情页面

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>
...
2.课程详情页面

因为接下来的组件中使用了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)
        ...
    

    数据库迁移指令

    3.后台接口

    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测试

    luffcc项目-08-课程详情页、CKEditor富文本编辑器、课程详情页面、后台接口_第1张图片

    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
    

    你可能感兴趣的:(8项目1,项目)