Bootstrap

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

在这里插入图片描述



前言

    随着数字化教育的兴起,构建一个高效、用户友好的线上教育平台至关重要。本文将探讨如何使用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
示例代码:

<video width="320" height="240" controls>
 <source src="http://sgigui51q.hb-bkt.clouddn.com/scenery.mp4" type="video/mp4">
</video> 

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学&nbsp;&nbsp;&nbsp;&nbsp;
              课程总时长:{{course.info.pub_lessons}}课时/{{course.info.lessons}}课时
              &nbsp;&nbsp;&nbsp;&nbsp;
              难度:{{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.效果图

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

在这里插入图片描述

;