【Django+Vue3 线上教育平台项目实战】构建课程详情页与集成视频播放功能

简介: 随着数字化教育的兴起,构建一个高效、用户友好的线上教育平台至关重要。本文将探讨如何使用Django与Vue.js 3结合,实现一个包含课程列表和课程详情页(含视频播放功能)的线上教育平台部分。本文主要介绍了如何设计数据库模型、处理数据查询、构建动态前端界面,并集成视频播放功能,为用户带来流畅的学习体验。

@TOC


前言

    随着数字化教育的兴起,构建一个高效、用户友好的线上教育平台至关重要。本文将探讨如何使用Django与Vue.js 3结合,实现一个包含课程列表和课程详情页(含视频播放功能)的线上教育平台部分。本文主要介绍了如何设计数据库模型、处理数据查询、构建动态前端界面,并集成视频播放功能,为用户带来流畅的学习体验。


一、课程列表页面

获取所有一级分类,获取所有二级分类,获取所有课程(课程分页处理),点击方向和分类时获取此方向或者此分类下的数据信息

页面展示:
在这里插入图片描述

a.后端代码

url配置信息:

    path('nav/cates/', CategoryView.as_view()), #课程列表页面 /project-方向/一级分类 ----- 侧边栏-获取一二级分类 -
    path('nav/category/', CateView.as_view()),  #课程列表页面 /project-二级分类
    #课程列表页面 / project
    path('courseSearch/', CourseSearch.as_view()),  # /project页面--搜索课程---

获取方向、分类及课程信息:

# 2.获取一、二级分类
class CategoryView(APIView):
    def get(self, request):
        # 查询所有一级分类:parent is null
        # query_set
        categories = CategoryModel.objects.filter(is_delete=0,parent__id__isnull=True)  #query_set

        clist = [] #侧边栏 二级分类显示几个
        for category in categories:
            # 获取一级下面所有的二级分类,操作显示二级分类数据条数
            sondata = category.son.all()[0:2] #query_set
            # d对二级数据进行序列化操作
            son = SonCategorySerializer(sondata, many=True)
            clist.append({
   "id": category.id, "name": category.name, "son": son.data})

        return Response({
   "code":"200", "data":clist})

# 2.2 categoryId指定类别时,展示categoryId的子分类
# 获取project页面的二级分类
class CateView(APIView):
    def get(self, request):
        categoryId = int(request.GET.get('categoryId'))
        print(categoryId)

        if categoryId:
            category = CategoryModel.objects.filter(is_delete=False, parent_id=categoryId).all()
        else:
            category = CategoryModel.objects.filter(is_delete=False, parent_id__isnull=False).all()
        cates = SonCategorySerializer(category, many=True)
        return Response({
   "cood": 200, "cateList": cates.data})

# 8.搜索课程
class CourseSearch(APIView):
    def get(self,request):
     topId = int(request.GET.get('topId'))
     cid = int(request.GET.get('cid'))
     page = int(request.GET.get('page'))
     pageSize = int(request.GET.get('pageSize'))
     print(page,pageSize)

     if topId:
         course = CourseModel.objects.filter(topid=topId)
     if cid:
         course = CourseModel.objects.filter(parent_id=cid)
     if not topId and not cid:
         course = CourseModel.objects.all()

     courseTotal = CourseSerializer(course,many=True)

     coursePage = Paginator(course, pageSize)
     courseList = CourseSerializer(coursePage.get_page(page),many=True)

     return Response({
   "code": 200,"pagetion":{
   "page":page,"pageSize":pageSize,"total":len(courseTotal.data)},'cousers': courseList.data})

b.前端代码

主要代码(方向、分类、课程的获取与展示)- src/views/Course.vue:

<div class="type">
    <div class="type-wrap">
        <!-- 方向: -->
        <div class="one warp">
            <span class="name">方向:</span>
            <ul class="items">
                <li :class="{cur: course.current_direction === 0}"><a href="" @click.prevent="course.current_direction=0">全部</a></li>
                <li :class="{cur: course.current_direction === direction.id}" v-for="direction in category.data"><a href="" @click.prevent="course.current_direction=direction.id">{
  {direction.name}}</a></li>
            </ul>
        </div>
        <!-- 分类 -->
        <div class="two warp">
            <span class="name">分类:</span>
            <ul class="items">
                <li :class="{cur: course.current_category === 0}"><a href="" @click.prevent="course.current_category=0">不限</a></li>
                <li :class="{cur: course.current_category === category.id}" v-for="category in category.cateList"><a href="" @click.prevent="course.current_category=category.id">{
  {category.name}}</a></li>
            </ul>
        </div>
    </div>
</div>
<!-- Main课程部分 -->
<div class="main">
    <div class="main-wrap">
        <div class="filter clearfix">
            <div class="sort l">
              <a href="" :class="{on:course.ordering==='-id'}" @click.prevent.stop="course.ordering=(course.ordering==='-id'?'':'-id')">最新</a>
              <a href="" :class="{on:course.ordering==='-students'}" @click.prevent.stop="course.ordering=(course.ordering==='-students'?'':'-students')">销量</a>
              <a href="" :class="{on:course.ordering==='-orders'}" @click.prevent.stop="course.ordering=(course.ordering==='-orders'?'':'-orders')">推荐</a>
            </div>
            <div class="other r clearfix"><a class="course-line l" href="" target="_blank">学习路线</a></div>
        </div>

        <ul class="course-list clearfix">
          <!-- 遍历展示课程信息 -->
          <li class="course-card" v-for="course_info in category.course_list">
            <router-link :to="`/project/${course_info.id}`">
                <div class="img"><img :src="course_info.picurl" alt=""></div>
                <p class="title ellipsis2">{
  {course_info.name}}</p>
                <p class="one">
                    <span>{
  { course_info.level }} · {
  { course_info.sales }}人报名</span>
                </p>
                <p class="two clearfix">
                    <span class="price l red bold" v-if="course_info.price !== undefined">¥{
  {parseFloat(course_info.price).toFixed(2)}}</span>
                    <span class="price l red bold" v-else>¥{
  {parseFloat(course_info.price).toFixed(2)}}</span>
                    <span class="origin-price l delete-line" v-if="course_info.price !== undefined">¥{
  {parseFloat(course_info.price).toFixed(2)}}</span>
                    <el-popconfirm title="您确认添加当前课程加入购物车吗?" @confirm.prevent.stop="add_course_to_cart(course_info)" confirmButtonText="买买买!" cancelButtonText="误操作!">
                      <template #reference>
                        <span class="add-shop-cart r" @click.stop.prevent=""><img class="icon imv2-shopping-cart" src="../assets/cart2.svg">加购物车</span>
                      </template>
                    </el-popconfirm>
                </p>
            </router-link>
          </li>
        </ul>

        <!-- 分页功能 -->
        <div class="page">
            <div style="position: absolute;left: 50%;transform: translateX(-50%)">
                <el-pagination
                style="margin: auto" background layout="prev, pager, next"
                :total='category.pageTion.total'
                :page-size="category.pageTion.pageSize"
                @current-change="change"/>
            </div>
        </div>

    </div>
</div>
import category from "../api/cetory.js";//++

category.get_category();
category.search_course(0, 0,pageTion);
category.get_cate(0);

src/api/cetory.js:

import {
    reactive } from "vue";
import http from "../http";
const category = reactive({
   
    data: [],  // 方向 / 一级分类
    course_list: [], // 课程信息
    cateList: [], //二级分类
    pageTion: {
   }, // 分页

    get_category(id) {
   
        return http.get("/home/nav/cates/", {
    params: {
    cateid: id } }).then(response => {
   
            //课程列表页面-project-获取方向(一级分类)
            // console.log("response.data.data*************/home/nav/cates/******************");
            // console.log(response.data.data);
            this.data = response.data.data;
        })
    },

    get_cate(categoryId) {
        return http.get("/home/nav/category/", { params: { categoryId: categoryId } }).then(response => {
            //课程列表页面-project-获取二级分类
            // console.log("response.data*********************/home/nav/category/***************************");
            // console.log(response.data);
            this.cateList = response.data.cateList;
        })
    },
    // 分页、搜索对应方向或分类的课程topid-->方向,cid-->分类
    search_course(topId, cid, page) {
        const params = {
            topId: topId,
            cid: cid,
            page: page.page,
            pageSize: page.pageSize
        }
        return http.get(`/home/courseSearch/`, { params }).then(response => {
            console.log("response.data****************/home/courseSearch/*********************");
            console.log(response.data);
            this.course_list = response.data.cousers;
            this.pageTion = response.data.pagetion;
        })
    },
})

export default category;

二、课程详情页面

a. 视频播放功能的集成

这里以七牛云服务器 (存储视频)+ vue-alipayer视频播放组件为例实现视频播放功能

1.获取上传视频的链接地址

具体操作步骤如下:

  • 1.七牛云注册登录:https://www.qiniu.com/
  • 2.点击对象存储:
    在这里插入图片描述
  • 3.创建存储空间:
    在这里插入图片描述
  • 4.创建成功:
    在这里插入图片描述
  • 5.上传一段视频用于在课程详情页面展示:
    在这里插入图片描述
  • 6.视频上传成功:
    在这里插入图片描述
  • 7.查看文件详情,可获得文件链接:
    在这里插入图片描述

2.集成在前端页面中

1>使用vue-alipayer视频播放组件

            <AliPlayerV3
              ref="player"
              class="h-64 md:h-96 w-full rounded-lg"
              style="height: 100%; width: 100%;"
              :source="course.info.course[0].video_url"
              :cover="course.info.course_cover"
              :options="options"
              @play="onPlay($event)"
              @pause="onPause($event)"
              @playing="onPlaying($event)"
            />

==source属性绑定的值,存放视频播放地址。==(通过向后端发送请求获取数据库中的数据)


页面效果如下图:
在这里插入图片描述

2>使用video标签

可参考菜鸟教程:https://www.runoob.com/html/html-videos.html
示例代码:
~~~html



b. 页面主要内容展示

1.后端代码

1>分析表

  • 1.课程表CourseModel
    • 新加字段:total_jie(总节数)、hours(总时长)、vide_url(课程总介绍)、question常见问题
  • 2.课程章表
    • 字段:id、名称、课程id(外键)、总节数、时间(用于页面展示)、总时长(秒)
  • 3.课程节表
    • 字段:id、名称、课程id、章id(外键)、视频id、时间、时长(秒)
  • 4.教师表(课程表+teacher字段关联教师表)
    • 字段:id、姓名、头像、介绍、教授的课程
  • 5.用户表
    • 字段:id、用户名、手机号、密码、积分、头像、个性签名
  • 6.评价表
    • 字段:id、userid(外键)、courseid(外键)、评价、评分
  • 7.回复表
    • 字段:id、回复人id(用户id)、评价id(外键)、内容

      2>核心逻辑

# 0.课程详情
class CourseDetailView(APIView):
    def get(self, request, id):
        r.delete_str("testdata")
        # 先取一下缓存
        test_data = r.get_str("testdata")
        if test_data:
            # 序列化 str-->json
            test_data = json.loads(test_data)
            return Response({
   "message":"test111111","data":test_data})

        course_list = CourseModel.objects.filter(id=id)
        ser = CourseSerializer(course_list, many=True)

        # 放入缓存 json-->str
        r.set_str('testdata',json.dumps(ser.data))

        return Response({
   "message":"test22222222222","code":"200","data":ser.data})

# 1.获取章节信息
class ChaptersView(APIView):
    def get(self, request, id):
        # 根据课程id查对应章节
        course = CourseModel.objects.filter(id=id).first()
        # course + chapters
        chapt = course.chapters.all()
        ser = ChaptersSerializer(chapt, many=True)
        return Response({
   "code": "200", "data": ser.data})

#2.评论及其回复
class CommentView(APIView):
    def get(self, request, id):
        # id---> 课程id ---对应查询课程下面的评论
        comments = CommentModel.objects.filter(course_id=id)
        comments_ser = CommentsSerializer(comments, many=True)
        return Response({
   "code": "200", "data": comments_ser.data})

2.前端代码

课程详情页面src/views/Info.vue:

<template>
    <div class="detail">
      <Header/>
      <!-- 主体内容 -->
      <div class="main">
        <!-- 课程详情 -上半部分 -->
        <div class="course-info">
          <div class="wrap-left">
              <!-- 视频播放器 -->
            <AliPlayerV3
              ref="player"
              class="h-64 md:h-96 w-full rounded-lg"
              style="height: 100%; width: 100%;"
              :source="course.info.course[0].video_url"
              :cover="course.info.course_cover"
              :options="options"
              @play="onPlay($event)"
              @pause="onPause($event)"
              @playing="onPlaying($event)"
            />
          </div>
          <div class="wrap-right">
            <h3 class="course-name">{
  {course.info.course[0].name}}</h3>
            <p class="data">
              {
  {course.info.course[0].sales}}人在A学    
              课程总时长:{
  {course.info.pub_lessons}}课时/{
  {course.info.lessons}}课时

              难度:{
  {course.info.course[0].level}}
            </p>

            <div class="sale-time" v-if="!course.info.discount.type">
              <p class="sale-type">课程价格 ¥{
  {parseFloat(course.info.course[0].price).toFixed(2)}}</p>
            </div>
            <p class="course-price" v-if="course.info.discount.price !== undefined">
              <span>活动价</span>
              <span class="discount">¥{
  {parseFloat(course.info.discount.price).toFixed(2)}}</span>
              <span class="original">¥{
  {parseFloat(course.info.price).toFixed(2)}}</span>
            </p>
            <p class="course-price" v-if="course.info.credit>0">
              <span>抵扣积分</span>
              <span class="discount">{
  {course.info.credit}}</span>
            </p>
            <div class="buy">
              <div class="buy-btn">
                <button class="buy-now">立即购买</button>
                <button class="free">免费试学</button>
              </div>
              <el-popconfirm title="您确认添加当前课程加入购物车吗?" @confirm="add_course_to_cart" confirmButtonText="买买买!" cancelButtonText="误操作!">
                <template #reference>
                  <div class="add-cart"><img src="../assets/cart-yellow.svg" alt="">加入购物车</div>
                </template>
              </el-popconfirm>
            </div>
          </div>
        </div>
        <!-- 课程标签、课程选项卡 -中间部分 -->
        <div class="course-tab">
          <ul class="tab-list">
            <li :class="course.tabIndex===1?'active':''" @click="course.tabIndex=1">详情介绍</li>
            <li :class="course.tabIndex===2?'active':''" @click="course.tabIndex=2">课程章节 <span :class="course.tabIndex!==2?'free':''" v-if="course.info.can_free_study">(试学)</span></li>
            <li :class="course.tabIndex===3?'active':''" @click="course.tabIndex=3">用户评论 </li>
            <li :class="course.tabIndex===4?'active':''" @click="course.tabIndex=4">常见问题</li>
          </ul>
        </div>
        <!-- 课程内容 -章节-下半部分 -->
        <!-- 章节:{
   {course.chapter_list[0].name}} -->
        <div class="course-content">
          <!-- 选项卡-内容 -->
          <div class="course-tab-list">
            <!-- 选项卡1:详情介绍 -->
            <div class="tab-item" v-if="course.tabIndex===1" v-html="course.info.course[0].describe"></div>
            <!-- 选项卡2:课程章节 -->
            <div class="tab-item" v-if="course.tabIndex===2">
              <div class="tab-item-title">
                <p class="chapter">课程章节</p>
                <p class="chapter-length">共{
  {course.chapter_list.length}}章 {
  {course.info.course[0].hours}}个课时</p>
              </div>
              <div class="chapter-item" v-for="chapter,index in course.chapter_list" :key="index">
                <p class="chapter-title"><img src="../assets/1.svg" alt="">第{
  {chapter.id}}章·{
  {chapter.name}}</p>
                <div class="chapter-title" style="padding-left: 2.4rem;" v-if="chapter.summary" v-html="chapter.summary"></div>
                <!-- jie:{
   {chapter.sections}} -->
                <ul class="lesson-list">
                  <li class="lesson-item"  v-for="lesson,index in chapter.sections" :key="index">
                    <p class="name">
                      <span class="index">{
  {chapter.orders}}-{
  {lesson.orders}}</span>
                      {
  {lesson.name}}
                      <span class="free" v-if="lesson.free_trail">免费</span>
                    </p>
                    <p class="time">{
  {lesson.duration}} <img src="../assets/chapter-player.svg"></p>
                    <button class="try"  v-if="lesson.free_trail">立即试学</button>
                    <button class="try" v-else>购买课程</button>
                  </li>
                </ul>
              </div>
            </div>
            <!-- 选项卡3:用户评论 -->
            <div class="tab-item" v-if="course.tabIndex===3">
              <h2>用户评论</h2>
                <div class="teacher-content">
                 <div class="cont1">
                   <img style="border-radius: 50%;" :src="course.comments_list[0].user.avatar">
                   <p class="teacher-name">{
  {course.comments_list[0].user.username}}</p>
                 </div>
                <div class="narrative" v-html="course.comments_list[0].message"></div>  
            </div>
            </div>

            <!-- 选项卡4:常见问题 -->
            <div class="tab-item" v-if="course.tabIndex===4">
              <h2>常见问题</h2>
              <div v-html="course.info.course[0].question"></div>
            </div>
          </div>

          <!-- 课程旁边的老师 -->
          <!-- 教师:{
   {course.info.course[0].teacher}} -->
          <div class="course-side">
             <div class="teacher-info">
               <h4 class="side-title"><span>授课老师</span></h4>
               <div class="teacher-content">
                 <div class="cont1">
                   <img style="border-radius: 50%;" :src="course.info.course[0].teacher.avatar">
                   <div class="name">
                     <p class="teacher-name">{
  {course.info.course[0].teacher.name}}</p>
                     <p class="teacher-title">{
  {course.info.course[0].teacher.get_role_display}}角色:教师,教授的课程:{
  {course.info.course[0].teacher.courses}}</p>
                   </div>
                 </div>
                 <div class="narrative" v-html="course.info.course[0].teacher.introduce"></div>
               </div>
             </div>
          </div>

        </div>
      </div>

      <Footer/>
    </div>
</template>

课程详情src/api/course.js:

  get_course() {
   
    // 获取课程详情
    return http.get(`/info/courses/${
     this.course_id}/`).then(response => {
   
      console.log("response.data:**************/info/courses/*******************");
      console.log(response.data);
      this.info.course = response.data.data;
      return this.get_course_chapters();
    })
  },
    get_course_chapters() {
   
    // 获取指定课程的章节列表
    return http.get(`/info/chapters/${
     this.course_id}/`).then(response => {
   
      // console.log("response.data---*******************/info/chapters********************");
      // console.log(response.data);
      this.chapter_list = response.data.data;
    })
  },
    get_comments_list() {
   
    // 获取对应课程下面的评论信息
    return http.get(`/info/comments/${
     this.course_id}/`).then(response => {
   
      console.log("response.data-----------/info/comments/*****************");
      console.log(response.data);
      console.log(response.data.data);
      this.comments_list = response.data.data;
    })
  },

3.效果图

  • 详情介绍
    在这里插入图片描述
  • 课程章节
    在这里插入图片描述
  • 用户评论
    在这里插入图片描述
  • 常见问题
    在这里插入图片描述

相关文章
|
1月前
|
传感器 监控 搜索推荐
智能服装:集成健康监测功能的纺织品——未来穿戴科技的新篇章
【10月更文挑战第7天】智能服装作为穿戴科技的重要分支,正以其独特的技术优势和广泛的应用前景,成为未来科技发展的亮点之一。它不仅改变了我们对服装的传统认知,更将健康监测、运动训练、医疗康复等功能融为一体,为我们的生活带来了更多的便利和可能。随着技术的不断进步和市场的日益成熟,我们有理由相信,智能服装将成为未来穿戴科技的新篇章,引领我们走向更加健康、智能、可持续的生活方式。
|
1月前
|
前端开发 JavaScript UED
探索Python Django中的WebSocket集成:为前后端分离应用添加实时通信功能
通过在Django项目中集成Channels和WebSocket,我们能够为前后端分离的应用添加实时通信功能,实现诸如在线聊天、实时数据更新等交互式场景。这不仅增强了应用的功能性,也提升了用户体验。随着实时Web应用的日益普及,掌握Django Channels和WebSocket的集成将为开发者开启新的可能性,推动Web应用的发展迈向更高层次的实时性和交互性。
75 1
|
12天前
|
程序员 API 数据库
Django/Flask深度揭秘:揭秘那些让程序员爱不释手的神奇功能!
在Web开发领域,Django与Flask凭借其独特魅力和强大功能深受程序员喜爱。Django作为全能型框架,以其ORM、模板引擎和丰富的内置功能著称;Flask则以轻量级、灵活的路由系统和强大的扩展生态见长。两者各具特色,为开发者提供了高效、灵活的开发工具。
35 4
|
18天前
|
JSON Java API
springboot集成ElasticSearch使用completion实现补全功能
springboot集成ElasticSearch使用completion实现补全功能
22 1
|
28天前
|
人工智能 JavaScript 网络安全
ToB项目身份认证AD集成(三完):利用ldap.js实现与windows AD对接实现用户搜索、认证、密码修改等功能 - 以及针对中文转义问题的补丁方法
本文详细介绍了如何使用 `ldapjs` 库在 Node.js 中实现与 Windows AD 的交互,包括用户搜索、身份验证、密码修改和重置等功能。通过创建 `LdapService` 类,提供了与 AD 服务器通信的完整解决方案,同时解决了中文字段在 LDAP 操作中被转义的问题。
|
1月前
|
数据采集 DataWorks 数据管理
DataWorks不是Excel,它是一个数据集成和数据管理平台
【10月更文挑战第10天】随着大数据技术的发展,企业对数据处理的需求日益增长。阿里云推出的DataWorks是一款强大的数据集成和管理平台,提供从数据采集、清洗、加工到应用的一站式解决方案。本文通过电商平台案例,详细介绍了DataWorks的核心功能和优势,展示了如何高效处理大规模数据,帮助企业挖掘数据价值。
81 1
|
1月前
|
数据采集 SQL DataWorks
DataWorks不是Excel,它是一个数据集成和数据管理平台
【10月更文挑战第5天】本文通过一家电商平台的案例,详细介绍了阿里云DataWorks在数据处理全流程中的应用。从多源数据采集、清洗加工到分析可视化,DataWorks提供了强大的一站式解决方案,显著提升了数据分析效率和质量。通过具体SQL示例,展示了如何构建高效的数据处理流程,突显了DataWorks相较于传统工具如Excel的优势,为企业决策提供了有力支持。
88 3
|
1月前
|
人工智能 自然语言处理 关系型数据库
阿里云云原生数据仓库 AnalyticDB PostgreSQL 版已完成和开源LLMOps平台Dify官方集成
近日,阿里云云原生数据仓库 AnalyticDB PostgreSQL 版已完成和开源LLMOps平台Dify官方集成。
|
1月前
|
存储 前端开发 Java
Spring Boot 集成 MinIO 与 KKFile 实现文件预览功能
本文详细介绍如何在Spring Boot项目中集成MinIO对象存储系统与KKFileView文件预览工具,实现文件上传及在线预览功能。首先搭建MinIO服务器,并在Spring Boot中配置MinIO SDK进行文件管理;接着通过KKFileView提供文件预览服务,最终实现文档管理系统的高效文件处理能力。
261 11
|
1月前
|
存储 Python
使用django构建一个多级评论功能
使用django构建一个多级评论功能
20 0