【22.0】课程页面搭建

发布时间 2023-08-19 17:25:38作者: Chimengmeng

【一】原型图

【二】前端页面搭建

【1】免费课页面

<template>
  <div class="course">
    <Header></Header>
    <div class="main">
      <!-- 筛选条件 -->
      <div class="condition">
        <ul class="cate-list">
          <li class="title">课程分类:</li>
          <li class="this">全部</li>
          <li>Python</li>
          <li>Linux运维</li>
          <li>Python进阶</li>
          <li>开发工具</li>
          <li>Go语言</li>
          <li>机器学习</li>
          <li>技术生涯</li>
        </ul>

        <div class="ordering">
          <ul>
            <li class="title">筛&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;选:</li>
            <li class="default this">默认</li>
            <li class="hot">人气</li>
            <li class="price">价格</li>
          </ul>
          <p class="condition-result">共21个课程</p>
        </div>

      </div>
      <!-- 课程列表 -->
      <div class="course-list">
        <div class="course-item">
          <div class="course-image">
            <img src="@/assets/img/course-cover.jpeg" alt="">
          </div>
          <div class="course-info">
            <h3>Python开发21天入门 <span><img src="@/assets/img/avatar1.svg" alt="">100人已加入学习</span></h3>
            <p class="teather-info">Alex 金角大王 老男孩Python教学总监 <span>共154课时/更新完成</span></p>
            <ul class="lesson-list">
              <li><span class="lesson-title">01 | 第1节:初识编码</span> <span class="free">免费</span></li>
              <li><span class="lesson-title">01 | 第1节:初识编码初识编码</span> <span class="free">免费</span></li>
              <li><span class="lesson-title">01 | 第1节:初识编码</span></li>
              <li><span class="lesson-title">01 | 第1节:初识编码初识编码</span></li>
            </ul>
            <div class="pay-box">
              <span class="discount-type">限时免费</span>
              <span class="discount-price">¥6.00元</span>
              <span class="original-price">原价:9.00元</span>
              <span class="buy-now">立即购买</span>
            </div>
          </div>
        </div>
        <div class="course-item">
          <div class="course-image">
            <img src="@/assets/img/course-cover.jpeg" alt="">
          </div>
          <div class="course-info">
            <h3>Python开发21天入门 <span><img src="@/assets/img/avatar1.svg" alt="">100人已加入学习</span></h3>
            <p class="teather-info">Alex 金角大王 老男孩Python教学总监 <span>共154课时/更新完成</span></p>
            <ul class="lesson-list">
              <li><span class="lesson-title">01 | 第1节:初识编码</span> <span class="free">免费</span></li>
              <li><span class="lesson-title">01 | 第1节:初识编码初识编码</span> <span class="free">免费</span></li>
              <li><span class="lesson-title">01 | 第1节:初识编码</span></li>
              <li><span class="lesson-title">01 | 第1节:初识编码初识编码</span></li>
            </ul>
            <div class="pay-box">
              <span class="discount-type">限时免费</span>
              <span class="discount-price">¥0.00元</span>
              <span class="original-price">原价:9.00元</span>
              <span class="buy-now">立即购买</span>
            </div>
          </div>
        </div>
        <div class="course-item">
          <div class="course-image">
            <img src="@/assets/img/course-cover.jpeg" alt="">
          </div>
          <div class="course-info">
            <h3>Python开发21天入门 <span><img src="@/assets/img/avatar1.svg" alt="">100人已加入学习</span></h3>
            <p class="teather-info">Alex 金角大王 老男孩Python教学总监 <span>共154课时/更新完成</span></p>
            <ul class="lesson-list">
              <li><span class="lesson-title">01 | 第1节:初识编码</span> <span class="free">免费</span></li>
              <li><span class="lesson-title">01 | 第1节:初识编码初识编码</span> <span class="free">免费</span></li>
              <li><span class="lesson-title">01 | 第1节:初识编码</span></li>
              <li><span class="lesson-title">01 | 第1节:初识编码初识编码</span></li>
            </ul>
            <div class="pay-box">
              <span class="discount-type">限时免费</span>
              <span class="discount-price">¥0.00元</span>
              <span class="original-price">原价:9.00元</span>
              <span class="buy-now">立即购买</span>
            </div>
          </div>
        </div>
        <div class="course-item">
          <div class="course-image">
            <img src="@/assets/img/course-cover.jpeg" alt="">
          </div>
          <div class="course-info">
            <h3>Python开发21天入门 <span><img src="@/assets/img/avatar1.svg" alt="">100人已加入学习</span></h3>
            <p class="teather-info">Alex 金角大王 老男孩Python教学总监 <span>共154课时/更新完成</span></p>
            <ul class="lesson-list">
              <li><span class="lesson-title">01 | 第1节:初识编码</span> <span class="free">免费</span></li>
              <li><span class="lesson-title">01 | 第1节:初识编码初识编码</span> <span class="free">免费</span></li>
              <li><span class="lesson-title">01 | 第1节:初识编码</span></li>
              <li><span class="lesson-title">01 | 第1节:初识编码初识编码</span></li>
            </ul>
            <div class="pay-box">
              <span class="discount-type">限时免费</span>
              <span class="discount-price">¥0.00元</span>
              <span class="original-price">原价:9.00元</span>
              <span class="buy-now">立即购买</span>
            </div>
          </div>
        </div>
      </div>
    </div>
    <Footer></Footer>
  </div>
</template>

<script>
import Header from "@/components/Header"
import Footer from "@/components/Footer"

export default {
  name: "Course",
  data() {
    return {
      category: 0,
    }
  },
  components: {
    Header,
    Footer,
  }
}
</script>

<style scoped>
.course {
  background: #f6f6f6;
}

.course .main {
  width: 1100px;
  margin: 35px auto 0;
}

.course .condition {
  margin-bottom: 35px;
  padding: 25px 30px 25px 20px;
  background: #fff;
  border-radius: 4px;
  box-shadow: 0 2px 4px 0 #f0f0f0;
}

.course .cate-list {
  border-bottom: 1px solid #333;
  border-bottom-color: rgba(51, 51, 51, .05);
  padding-bottom: 18px;
  margin-bottom: 17px;
}

.course .cate-list::after {
  content: "";
  display: block;
  clear: both;
}

.course .cate-list li {
  float: left;
  font-size: 16px;
  padding: 6px 15px;
  line-height: 16px;
  margin-left: 14px;
  position: relative;
  transition: all .3s ease;
  cursor: pointer;
  color: #4a4a4a;
  border: 1px solid transparent; /* transparent 透明 */
}

.course .cate-list .title {
  color: #888;
  margin-left: 0;
  letter-spacing: .36px;
  padding: 0;
  line-height: 28px;
}

.course .cate-list .this {
  color: #ffc210;
  border: 1px solid #ffc210 !important;
  border-radius: 30px;
}

.course .ordering::after {
  content: "";
  display: block;
  clear: both;
}

.course .ordering ul {
  float: left;
}

.course .ordering ul::after {
  content: "";
  display: block;
  clear: both;
}

.course .ordering .condition-result {
  float: right;
  font-size: 14px;
  color: #9b9b9b;
  line-height: 28px;
}

.course .ordering ul li {
  float: left;
  padding: 6px 15px;
  line-height: 16px;
  margin-left: 14px;
  position: relative;
  transition: all .3s ease;
  cursor: pointer;
  color: #4a4a4a;
}

.course .ordering .title {
  font-size: 16px;
  color: #888;
  letter-spacing: .36px;
  margin-left: 0;
  padding: 0;
  line-height: 28px;
}

.course .ordering .this {
  color: #ffc210;
}

.course .ordering .price {
  position: relative;
}

.course .ordering .price::before,
.course .ordering .price::after {
  cursor: pointer;
  content: "";
  display: block;
  width: 0px;
  height: 0px;
  border: 5px solid transparent;
  position: absolute;
  right: 0;
}

.course .ordering .price::before {
  border-bottom: 5px solid #aaa;
  margin-bottom: 2px;
  top: 2px;
}

.course .ordering .price::after {
  border-top: 5px solid #aaa;
  bottom: 2px;
}

.course .course-item:hover {
  box-shadow: 4px 6px 16px rgba(0, 0, 0, .5);
}

.course .course-item {
  width: 1100px;
  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);
  /* css3.0 过渡动画 hover 事件操作 */
  transition: all .2s ease;
}

.course .course-item::after {
  content: "";
  display: block;
  clear: both;
}

/* 顶级元素 父级元素  当前元素{} */
.course .course-item .course-image {
  float: left;
  width: 423px;
  height: 210px;
  margin-right: 30px;
}

.course .course-item .course-image img {
  width: 100%;
}

.course .course-item .course-info {
  float: left;
  width: 596px;
}

.course-item .course-info h3 {
  font-size: 26px;
  color: #333;
  font-weight: normal;
  margin-bottom: 8px;
}

.course-item .course-info h3 span {
  font-size: 14px;
  color: #9b9b9b;
  float: right;
  margin-top: 14px;
}

.course-item .course-info h3 span img {
  width: 11px;
  height: auto;
  margin-right: 7px;
}

.course-item .course-info .teather-info {
  font-size: 14px;
  color: #9b9b9b;
  margin-bottom: 14px;
  padding-bottom: 14px;
  border-bottom: 1px solid #333;
  border-bottom-color: rgba(51, 51, 51, .05);
}

.course-item .course-info .teather-info span {
  float: right;
}

.course-item .lesson-list::after {
  content: "";
  display: block;
  clear: both;
}

.course-item .lesson-list li {
  float: left;
  width: 44%;
  font-size: 14px;
  color: #666;
  padding-left: 22px;
  /* background: url("路径") 是否平铺 x轴位置 y轴位置 */
  background: url("/src/assets/img/play-icon-gray.svg") no-repeat left 4px;
  margin-bottom: 15px;
}

.course-item .lesson-list li .lesson-title {
  /* 以下3句,文本内容过多,会自动隐藏,并显示省略符号 */
  text-overflow: ellipsis;
  overflow: hidden;
  white-space: nowrap;
  display: inline-block;
  max-width: 200px;
}

.course-item .lesson-list li:hover {
  background-image: url("/src/assets/img/play-icon-yellow.svg");
  color: #ffc210;
}

.course-item .lesson-list li .free {
  width: 34px;
  height: 20px;
  color: #fd7b4d;
  vertical-align: super;
  margin-left: 10px;
  border: 1px solid #fd7b4d;
  border-radius: 2px;
  text-align: center;
  font-size: 13px;
  white-space: nowrap;
}

.course-item .lesson-list li:hover .free {
  color: #ffc210;
  border-color: #ffc210;
}

.course-item .pay-box::after {
  content: "";
  display: block;
  clear: both;
}

.course-item .pay-box .discount-type {
  padding: 6px 10px;
  font-size: 16px;
  color: #fff;
  text-align: center;
  margin-right: 8px;
  background: #fa6240;
  border: 1px solid #fa6240;
  border-radius: 10px 0 10px 0;
  float: left;
}

.course-item .pay-box .discount-price {
  font-size: 24px;
  color: #fa6240;
  float: left;
}

.course-item .pay-box .original-price {
  text-decoration: line-through;
  font-size: 14px;
  color: #9b9b9b;
  margin-left: 10px;
  float: left;
  margin-top: 10px;
}

.course-item .pay-box .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;
  text-align: center;
  line-height: 38px;
}

.course-item .pay-box .buy-now:hover {
  color: #fff;
  background: #ffc210;
  border: 1px solid #ffc210;
}
</style>

【2】轻课页面

<script setup>

</script>

<template>
    <div>
      <Header></Header>
        <h1>轻课</h1>
      <Footer></Footer>
    </div>
</template>
<script>
import Header from "@/components/Header.vue";
import Footer from "@/components/Footer.vue";
export default {
    name: 'LightCourseView',
  components: {
    Header,
    Footer
  }
}
</script>
<style scoped>

</style>

【3】实战课

<script setup>

</script>

<template>
  <div>
    <Header></Header>
    <h1>实战课</h1>
    <Footer></Footer>
  </div>
</template>

<script>
import Header from "@/components/Header.vue";
import Footer from "@/components/Footer.vue";

export default {
  name: 'ActualCourseView',
  components: {
    Header,
    Footer
  }
}
</script>
<style scoped>

</style>

【4】注册路由

import Vue from 'vue'
import VueRouter from 'vue-router'
import HomeView from '../views/HomeView.vue'
import SkillView from "@/views/SkillView.vue";
import LightCourseView from "@/views/LightCourseView.vue";
import FreeCourseView from "@/views/FreeCourseView.vue";
import ActualCourseView from "@/views/ActualCourseView.vue";

Vue.use(VueRouter)

const routes = [
    {
        path: '/',
        name: 'home',
        component: HomeView
    },
    {
        path: '/sckill',
        name: 'sckill',
        component: SkillView
    },
    // 实战课
    {
        path: '/actual-course',
        name: 'actual-course',
        component: ActualCourseView
    },
    // 免费课
    {
        path: '/free-course',
        name: 'free-course',
        component: FreeCourseView
    },
    // 轻课
    {
        path: '/light-course',
        name: 'light-course',
        component: LightCourseView
    },
]

const router = new VueRouter({
    mode: 'history',
    base: process.env.BASE_URL,
    routes
})

export default router

【三】课程相关表分析

【1】课程分类表

  • 课程分类表:id,分类名,继承BaseModel
  • 跟课程表是一对多,一个分类下有很多课程

【2】课程表(实战课课表)

  • 如果一个表:
    • 不同课程直接,字段可能不一样
  • 多个课程,多个表
    • 只写实战课这条线

【3】章节表

  • 跟课程是一对多

【4】课时表

  • 跟章节是一对多

【5】老师表

  • 跟课程一对多

【四】表的关联关系

  • 公司中外键都是逻辑外键,不建物理外键
  • 靠一个东西建立出来的三种关系(外键)
    • 一对一
    • 一对多
    • 多对多

【五】表模型的创建

【1】基表

# -*-coding: Utf-8 -*-
# @File : common_models .py
# author: Chimengmeng
# blog_url : https://www.cnblogs.com/dream-ze/
# Time:2023/8/8
from django.db import models


class BaseModel(models.Model):
    '''
        公共字段创建基表 ---- 其他表也可能用
        id img图片地址 上传时间 是否删除 是否显示 显示排序
    '''
    created_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')
    updated_time = models.DateTimeField(auto_now=True, verbose_name='最后更新时间')
    is_delete = models.BooleanField(default=False, verbose_name='是否删除')
    is_show = models.BooleanField(default=True, verbose_name='是否上架')
    orders = models.IntegerField(verbose_name='优先级')

    class Meta:
        abstract = True  # 这个表模型只用来继承,不用来在数据库中生成表

【2】课程分类

# 课程分类
class CourseCategory(BaseModel):
    """分类  ID  name  """
    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

【3】课程表

# 课程表
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="课程状态")
    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)
    teacher = models.ForeignKey("Teacher", on_delete=models.DO_NOTHING, null=True, blank=True, verbose_name="授课老师")
    course_category = models.ForeignKey("CourseCategory", on_delete=models.SET_NULL, db_constraint=False, null=True,
                                        blank=True,
                                        verbose_name="课程分类")

    class Meta:
        db_table = "luffy_course"
        verbose_name = "课程"
        verbose_name_plural = "课程"

    def __str__(self):
        return "%s" % self.name

【4】章节表

# 章节表
class CourseChapter(BaseModel):
    """章节"""
    '''
    基于对象的跨表查询 : 
        正 :通过字段查询
        反 : 表名小写(一对一:表名小写,一对多:表名小写_set.all())
            related_name  : 基于对象跨表反向查询,替换表名小写
    基于双下划线的联表查询 : 正 反
            related_query_name = 'xxx' : 基于联表反向查询,替换表名小写
    '''
    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)

    course = models.ForeignKey("Course", related_name='coursechapters', on_delete=models.CASCADE,
                               verbose_name="课程名称")

    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)

【5】课时表

# 课时表
class CourseSection(BaseModel):
    """课时"""
    section_type_choices = (
        (0, '文档'),
        (1, '练习'),
        (2, '视频')
    )
    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)

    chapter = models.ForeignKey("CourseChapter", related_name='coursesections', on_delete=models.CASCADE,
                                verbose_name="课程章节")

    class Meta:
        db_table = "luffy_course_section"
        verbose_name = "课时"
        verbose_name_plural = verbose_name

    def __str__(self):
        return "%s-%s" % (self.chapter, self.name)

【6】导师表

# 导师表
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】资源手动迁移

# 头像图片放在 media/teacher 文件夹下
# 课程图片放在 media/course 文件夹下

【2】老师表

INSERT INTO luffy_teacher(id, orders, is_show, is_delete, created_time, updated_time, name, role, title, signature, image, brief) VALUES (1, 1, 1, 0, '2022-07-14 13:44:19.661327', '2022-07-14 13:46:54.246271', 'Alex', 1, '老男孩Python教学总监', '金角大王', 'teacher/alex_icon.png', '老男孩教育CTO & CO-FOUNDER 国内知名PYTHON语言推广者 51CTO学院2016\2017年度最受学员喜爱10大讲师之一 多款开源软件作者 曾任职公安部、飞信、中金公司、NOKIA中国研究院、华尔街英语、ADVENT、汽车之家等公司');

INSERT INTO luffy_teacher(id, orders, is_show, is_delete, created_time, updated_time, name, role, title, signature, image, brief) VALUES (2, 2, 1, 0, '2022-07-14 13:45:25.092902', '2022-07-14 13:45:25.092936', 'Mjj', 0, '前美团前端项目组架构师', NULL, 'teacher/mjj_icon.png', '是马JJ老师, 一个集美貌与才华于一身的男人,搞过几年IOS,又转了前端开发几年,曾就职于美团网任高级前端开发,后来因为不同意王兴(美团老板)的战略布局而出家做老师去了,有丰富的教学经验,开起车来也毫不含糊。一直专注在前端的前沿技术领域。同时,爱好抽烟、喝酒、烫头(锡纸烫)。 我的最爱是前端,因为前端妹子多。');

INSERT INTO luffy_teacher(id, orders, is_show, is_delete, created_time, updated_time, name, role, title, signature, image, brief) VALUES (3, 3, 1, 0, '2022-07-14 13:46:21.997846', '2022-07-14 13:46:21.997880', 'Lyy', 0, '老男孩Linux学科带头人', NULL, 'teacher/lyy_icon.png', 'Linux运维技术专家,老男孩Linux金牌讲师,讲课风趣幽默、深入浅出、声音洪亮到爆炸');

【3】分类表

INSERT INTO luffy_course_category(id, orders, is_show, is_delete, created_time, updated_time, name) VALUES (1, 1, 1, 0, '2022-07-14 13:40:58.690413', '2022-07-14 13:40:58.690477', 'Python');

INSERT INTO luffy_course_category(id, orders, is_show, is_delete, created_time, updated_time, name) VALUES (2, 2, 1, 0, '2022-07-14 13:41:08.249735', '2022-07-14 13:41:08.249817', 'Linux');

【4】课程表

INSERT INTO luffy_course(id, orders, is_show, is_delete, created_time, updated_time, name, course_img, course_type, brief, level, pub_date, period, attachment_path, status, students, sections, pub_sections, price, course_category_id, teacher_id) VALUES (1, 1, 1, 0, '2022-07-14 13:54:33.095201', '2022-07-14 13:54:33.095238', 'Python开发21天入门', 'courses/alex_python.png', 0, 'Python从入门到入土&&&Python从入门到入土&&&Python从入门到入土&&&Python从入门到入土&&&Python从入门到入土&&&Python从入门到入土&&&Python从入门到入土&&&Python从入门到入土&&&Python从入门到入土&&&Python从入门到入土&&&Python从入门到入土&&&Python从入门到入土', 0, '2022-07-14', 21, '', 0, 231, 120, 120, 0.00, 1, 1);

INSERT INTO luffy_course(id, orders, is_show, is_delete, created_time, updated_time, name, course_img, course_type, brief, level, pub_date, period, attachment_path, status, students, sections, pub_sections, price, course_category_id, teacher_id) VALUES (2, 2, 1, 0, '2022-07-14 13:56:05.051103', '2022-07-14 13:56:05.051142', 'Python项目实战', 'courses/mjj_python.png', 0, '', 1, '2022-07-14', 30, '', 0, 340, 120, 120, 99.00, 1, 2);

INSERT INTO luffy_course(id, orders, is_show, is_delete, created_time, updated_time, name, course_img, course_type, brief, level, pub_date, period, attachment_path, status, students, sections, pub_sections, price, course_category_id, teacher_id) VALUES (3, 3, 1, 0, '2022-07-14 13:57:21.190053', '2022-07-14 13:57:21.190095', 'Linux系统基础5周入门精讲', 'courses/lyy_linux.png', 0, '', 0, '2022-07-14', 25, '', 0, 219, 100, 100, 39.00, 2, 3);

【5】章节表

INSERT INTO luffy_course_chapter(id, orders, is_show, is_delete, created_time, updated_time, chapter, name, summary, pub_date, course_id) VALUES (1, 1, 1, 0, '2022-07-14 13:58:34.867005', '2022-07-14 14:00:58.276541', 1, '计算机原理', '', '2022-07-14', 1);

INSERT INTO luffy_course_chapter(id, orders, is_show, is_delete, created_time, updated_time, chapter, name, summary, pub_date, course_id) VALUES (2, 2, 1, 0, '2022-07-14 13:58:48.051543', '2022-07-14 14:01:22.024206', 2, '环境搭建', '', '2022-07-14', 1);

INSERT INTO luffy_course_chapter(id, orders, is_show, is_delete, created_time, updated_time, chapter, name, summary, pub_date, course_id) VALUES (3, 3, 1, 0, '2022-07-14 13:59:09.878183', '2022-07-14 14:01:40.048608', 1, '项目创建', '', '2022-07-14', 2);

INSERT INTO luffy_course_chapter(id, orders, is_show, is_delete, created_time, updated_time, chapter, name, summary, pub_date, course_id) VALUES (4, 4, 1, 0, '2022-07-14 13:59:37.448626', '2022-07-14 14:01:58.709652', 1, 'Linux环境创建', '', '2022-07-14', 3);

【6】课时表

INSERT INTO luffy_course_Section(id, is_show, is_delete, created_time, updated_time, name, orders, section_type, section_link, duration, pub_date, free_trail, chapter_id) VALUES (1, 1, 0, '2022-07-14 14:02:33.779098', '2022-07-14 14:02:33.779135', '计算机原理上', 1, 2, NULL, NULL, '2022-07-14 14:02:33.779193', 1, 1);

INSERT INTO luffy_course_Section(id, is_show, is_delete, created_time, updated_time, name, orders, section_type, section_link, duration, pub_date, free_trail, chapter_id) VALUES (2, 1, 0, '2022-07-14 14:02:56.657134', '2022-07-14 14:02:56.657173', '计算机原理下', 2, 2, NULL, NULL, '2022-07-14 14:02:56.657227', 1, 1);

INSERT INTO luffy_course_Section(id, is_show, is_delete, created_time, updated_time, name, orders, section_type, section_link, duration, pub_date, free_trail, chapter_id) VALUES (3, 1, 0, '2022-07-14 14:03:20.493324', '2022-07-14 14:03:52.329394', '环境搭建上', 1, 2, NULL, NULL, '2022-07-14 14:03:20.493420', 0, 2);

INSERT INTO luffy_course_Section(id, is_show, is_delete, created_time, updated_time, name, orders, section_type, section_link, duration, pub_date, free_trail, chapter_id) VALUES (4, 1, 0, '2022-07-14 14:03:36.472742', '2022-07-14 14:03:36.472779', '环境搭建下', 2, 2, NULL, NULL, '2022-07-14 14:03:36.472831', 0, 2);

INSERT INTO luffy_course_Section(id, is_show, is_delete, created_time, updated_time, name, orders, section_type, section_link, duration, pub_date, free_trail, chapter_id) VALUES (5, 1, 0, '2022-07-14 14:04:19.338153', '2022-07-14 14:04:19.338192', 'web项目的创建', 1, 2, NULL, NULL, '2022-07-14 14:04:19.338252', 1, 3);

INSERT INTO luffy_course_Section(id, is_show, is_delete, created_time, updated_time, name, orders, section_type, section_link, duration, pub_date, free_trail, chapter_id) VALUES (6, 1, 0, '2022-07-14 14:04:52.895855', '2022-07-14 14:04:52.895890', 'Linux的环境搭建', 1, 2, NULL, NULL, '2022-07-14 14:04:52.895942', 1, 4);

【7】注册后台

from django.contrib import admin

from luffyCity.apps.course.models import CourseCategory, CourseSection, CourseChapter, Teacher, Course

# Register your models here.

##### 懒人写法 ####
# admin.site.register(CourseCategory)
# admin.site.register(CourseSection)
# admin.site.register(CourseChapter)


# admin.site.register(Teacher)
# admin.site.register(Course)

#  自定制后台显示内容 #
@admin.register(CourseCategory)
class UserAdmin(admin.ModelAdmin):
    list_display = ['id', 'name']


#
@admin.register(Course)
class UserAdmin(admin.ModelAdmin):
    list_display = ['id',
                    'name',
                    'course_img',
                    # 'brief',
                    'attachment_path',
                    'pub_sections',
                    'price',
                    'students',
                    'period',
                    'sections',
                    'course_type',
                    'level',
                    'status',
                    'teacher',
                    'sections',
                    ]


@admin.register(Teacher)
class UserAdmin(admin.ModelAdmin):
    list_display = ['id', 'name', 'title', 'signature', 'image', 'brief']


#
@admin.register(CourseChapter)
class UserAdmin(admin.ModelAdmin):
    list_display = ['id', 'name', 'chapter', 'summary', 'pub_date']


@admin.register(CourseSection)
class UserAdmin(admin.ModelAdmin):
    list_display = ['id', 'name', 'section_type', 'duration', 'section_link', 'free_trail']

【七】课程主页接口

【1】课程分类接口

  • luffyCity\luffyCity\apps\course\serializers\CourseCategorySerializer.py
# -*-coding: Utf-8 -*-
# @File : CourseCategorySerializer .py
# author: Chimengmeng
# blog_url : https://www.cnblogs.com/dream-ze/
# Time:2023/8/11

from rest_framework import serializers
from ..models import CourseCategory, Course, CourseChapter, CourseSection, Teacher


class CourseCategorySerializer(serializers.ModelSerializer):
    class Meta:
        model = CourseCategory
        fields = ['id', 'name']
  • luffyCity\luffyCity\apps\course\views.py
from django.shortcuts import render
from rest_framework.viewsets import GenericViewSet
from .models import CourseCategory, Course, CourseChapter, CourseSection, Teacher
from .serializers.CourseCategorySerializer import CourseCategorySerializer
from ...utils.common_mixin import CommonListModelMixin as ListModelMixin


class CourseCategoryView(GenericViewSet, ListModelMixin):
    # 查询所有接口
    queryset = CourseCategory.objects.filter(is_delete=False, is_show=True).order_by("orders")
    serializer_class = CourseCategorySerializer
  • luffyCity\luffyCity\apps\course\urls.py
# -*-coding: Utf-8 -*-
# @File : urls .py
# author: Chimengmeng
# blog_url : https://www.cnblogs.com/dream-ze/
# Time:2023/8/8
from django.urls import path, include
from rest_framework.routers import SimpleRouter

from luffyCity.apps.course.views import CourseCategoryView

router = SimpleRouter()
router.register('category', CourseCategoryView, 'category')

urlpatterns = [
]
urlpatterns += router.urls

【2】查询所有课程接口

  • 带过滤
    • 按分类过滤
    • 借助 django-filter
  • 带排序
  • 带分页
  • luffyCity\luffyCity\apps\course\views.py
class CourseView(GenericViewSet, ListModelMixin):
    # 查询所有接口
    queryset = Course.objects.filter(is_delete=False, is_show=True).order_by("orders")
    # 序列化
    serializer_class = CourseSerializer
    # 分页
    pagination_class = CommonPageNumberPagination
    # 排序
    filter_backends = [OrderingFilter, DjangoFilterBackend]
    # OrderingFilter 排序字段
    ordering_fields = ['student', 'price']
    # 过滤 : 课程分类 ---- course_category=1
    # DjangoFilterBackend 过滤字段
    filterset_fields = ['course_category']
  • luffyCity\luffyCity\apps\course\serializers\CommonSerializers.py
# 只用来做序列化
class CourseSerializer(serializers.ModelSerializer):
    # 重写,返回老师向详情  ---- 子序列化类 --- 在序列化类中重写
    # 直接在 子序列化类 中对字段进行序列化
    teacher = TeacherSerializer()

    class Meta:
        model = Course
        fields = ['id',
                  'name',
                  'course_img',
                  'brief',
                  'attachment_path',  # 课件路径地址
                  'pub_sections',  # 发布课时数
                  'price',
                  'students',
                  'period',  # 建议学习周期
                  'sections',  # 总课课时数
                  'course_type_name',  # 课程类型 -- 中文
                  'level_name',  # 难度等级 -- 中文
                  'status_name',  # 课程状态 -- 中文
                  'teacher',  # 重写,返回老师向详情
                  'section_list',  # 重写,最多拿4个课时
                  ]

  • luffyCity\luffyCity\apps\course\models.py
from django.db import models

from luffyCity.utils.common_models import BaseModel


# 课程分类
class CourseCategory(BaseModel):
    """分类  ID  name  """
    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="课程状态")
    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)
    teacher = models.ForeignKey("Teacher", on_delete=models.DO_NOTHING, null=True, blank=True, verbose_name="授课老师")
    course_category = models.ForeignKey("CourseCategory", on_delete=models.SET_NULL, db_constraint=False, null=True,
                                        blank=True,
                                        verbose_name="课程分类")

    class Meta:
        db_table = "luffy_course"
        verbose_name = "课程"
        verbose_name_plural = "课程"

    def __str__(self):
        return "%s" % self.name

    # 课程类型 -- 中文
    def course_type_name(self):
        return self.get_course_type_display()  # 拿到 course_type 对应的中文

    # 难度等级 -- 中文
    def level_name(self):
        return self.get_level_display()

    # 课程状态 -- 中文
    def status_name(self):
        return self.get_status_display()

    # 重写,返回老师向详情  ---- 子序列化 --- 在序列化类中重写

    # 重写,最多拿4个课时
    def section_list(self):
        l = []
        # 最多拿4个
        # 根据课程获取所有章节

        # 当 l 的长度大于等于 4 时 ,直接返回

        # 写了related_name 可以直接按字段名取 ---- related_name  : 基于对象跨表反向查询,替换表名小写
        # 否则是 coursechapter.set_all()
        course_chapter_list = self.coursechapters.all()

        # 循环所有章节,拿出所有课时,追加到 l 中
        # 循环所有章节拿每一个章节
        for course_chapter in course_chapter_list:
            # course_chapter 根据每一个章节拿课时
            chapter_section_list = course_chapter.coursesections.all()
            # 循环所有课时追加课时列表
            for chapter_section in chapter_section_list:
                l.append({
                    'id': chapter_section.pk,
                    'name': chapter_section.name,
                    'section_link': chapter_section.section_link,
                    'duration': chapter_section.duration,
                    'free_trail': chapter_section.free_trail,
                })

                if len(l) >= 4:
                    return l
        return l


# 导师表
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

    def role_name(self):
        return self.get_role_display()


# 章节表
class CourseChapter(BaseModel):
    """章节"""
    '''
    基于对象的跨表查询 : 
        正 :通过字段查询
        反 : 表名小写(一对一:表名小写,一对多:表名小写_set.all())
            related_name  : 基于对象跨表反向查询,替换表名小写
    基于双下划线的联表查询 : 正 反
            related_query_name = 'xxx' : 基于联表反向查询,替换表名小写
    '''
    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)

    course = models.ForeignKey("Course", related_name='coursechapters', on_delete=models.CASCADE,
                               verbose_name="课程名称")

    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, '视频')
    )
    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)

    chapter = models.ForeignKey("CourseChapter", related_name='coursesections', on_delete=models.CASCADE,
                                verbose_name="课程章节")

    class Meta:
        db_table = "luffy_course_section"
        verbose_name = "课时"
        verbose_name_plural = verbose_name

    def __str__(self):
        return "%s-%s" % (self.chapter, self.name)

【3】课程详情接口

  • 方式一:
    • 直接在原来查询所有的视图类上加入继承:RetrieveModelMixin
    • 继续使用查询所有的序列化类(没有章节及课时)
    • 配合一个通过课程id,查询所有课程章节及课时的接口
  • 方式二:
    • 直接在原来查询所有的视图类上加入继承:RetrieveModelMixin
    • 重写序列化类:返回课程详情,返回课程章节及课时

【4】课程详情后台之所有章节接口

  • 该接口是为了配合 :课程详情页面写
  • 写的接口是查询所有章节及课时
    • 需要按照课程id过滤
  • 写查询所有章节接口(带过滤)
  • luffyCity\luffyCity\apps\course\views.py
class CourseChapterView(GenericViewSet, ListModelMixin):
    # 查询所有接口
    queryset = CourseChapter.objects.filter(is_delete=False, is_show=True).order_by("orders")
    # 序列化
    serializer_class = CourseChapterSerializer
    # 排序
    filter_backends = [DjangoFilterBackend]
    # 过滤 : 课程分类 ---- course_category=1
    # DjangoFilterBackend 过滤字段 按课程过滤
    filterset_fields = ['course']
  • luffyCity\luffyCity\apps\course\serializers\CommonSerializers.py
class CourseChapterSerializer(serializers.ModelSerializer):
    '''
    拿出章节下所有课时
    方案一:
        表模型中写 section_list 方法,返回列表
    方案二:
        序列化类中写
         section_list = serializers.SerializerMethodField()
        def get_section_list(self, obj):
            return []
    方案三:
        子序列化类

    '''

    # 子序列化类(在章节表中隐藏了通过 related_name 写的 coursesections 字段 ,他可以通过 章节对象.coursesections拿到)
    # 章节下的课时是多条 , 使用子序列化 , instance 不用传,但是many参数需要传
    coursesections = CourseSectionSerializer(many=True)

    class Meta:
        model = CourseChapter
        fields = ['id', 'name', 'chapter', 'summary', 'pub_date','coursesections']
  • luffyCity\luffyCity\apps\course\urls.py
# -*-coding: Utf-8 -*-
# @File : urls .py
# author: Chimengmeng
# blog_url : https://www.cnblogs.com/dream-ze/
# Time:2023/8/8
from django.urls import path, include
from rest_framework.routers import SimpleRouter

from luffyCity.apps.course.views import CourseCategoryView, CourseView, CourseChapterView

router = SimpleRouter()
router.register('category', CourseCategoryView, 'category')
router.register('course', CourseView, 'course')
router.register('chapter', CourseChapterView, 'chapter')

urlpatterns = [
]
urlpatterns += router.urls

【八】前端页面搭建

  • 借助第三方
cnpm install -S vue-core-video-player 
  • 配置
import VueCoreVideoPlayer from 'vue-core-video-player'
//或者
Vue.use(VueCoreVideoPlayer, {
  lang: 'zh-CN'
})

【视频上传】

【1】正常项目上传流程

  • 在正常的项目上传流程中,涉及到前端和后端的配合。具体步骤如下:

    • 前端选择视频:用户在前端页面选择要上传的视频文件。

    • 传输到后端:前端通过与后端建立通信,将选定的视频文件传输给后端服务器。

    • 后端传输至七牛云:后端服务器调用文件上传接口,将接收到的视频文件传输至七牛云存储服务。

    • 返回地址:七牛云存储服务在上传完成后会返回一个视频文件在七牛云上的访问地址。

    • 存储至数据库:后端服务器将返回的访问地址存储到数据库中,以便后续使用。

【2】如何使用Python将文件传到七牛云:

from qiniu import Auth, put_file

q = Auth('', '')
#要上传的空间
bucket_name = 'luffy-storge'
#上传后保存的文件名
key = '致命诱惑.mp4'
#生成上传 Token,可以指定过期时间等
token = q.upload_token(bucket_name, key, 3600)
#要上传文件的本地路径
localfile = './1.mp4'
ret, info = put_file(token, key, localfile, version='v2')
print(info)
print(ret)
print('http://rx7c1tgq2.hd-bkt.clouddn.com/'+key)
  • 以上给出了一个使用Python将文件传到七牛云的示例代码。代码中使用了七牛云提供的Python SDK库,主要包括qiniuput_file模块。
  • 下面对代码进行逐行解释:
from qiniu import Auth, put_file

q = Auth('', '')
  • 这段代码引入了七牛云提供的Authput_file模块,并创建了一个Auth对象,用于生成上传凭证。
# 要上传的空间
bucket_name = 'luffy-storge'
# 上传后保存的文件名
key = '致命**.mp4'
  • 在代码中,bucket_name变量代表要上传的存储空间名称,key变量代表上传后保存的文件名。
# 生成上传 Token,可以指定过期时间等
token = q.upload_token(bucket_name, key, 3600)
  • 通过调用Auth对象的upload_token方法生成上传凭证,该凭证包含了七牛云的授权信息。
# 要上传文件的本地路径
localfile = './1.mp4'
  • 在示例代码中,localfile变量表示要上传的文件的本地路径。
ret, info = put_file(token, key, localfile, version='v2')
print(info)
print(ret)
print('http://rx7c1tgq2.hd-bkt.clouddn.com/'+key)
  • 最后通过调用put_file方法实现文件上传,将本地文件上传至七牛云,并返回上传结果。ret变量表示上传的结果信息,info变量表示请求的详细信息。最后一行代码打印出了上传成功后的访问地址。