【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.效果图

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

相关文章
|
12月前
|
存储 缓存 NoSQL
深入理解Django与Redis的集成实践
深入理解Django与Redis的集成实践
373 0
|
4月前
|
运维 安全 关系型数据库
【产品升级】Dataphin V5.1版本发布:跨云数据集成、指标管理、平台运维带来重大更新!
V5.1版本新增多项功能:对接AWS生态(支持Amazon EMR、Redshift等),强化研发技术支撑(如API认证升级、全量任务隔离),完善运营消费链路(新增业务指标管理、指标关系图),提升平台综合能力(自定义菜单、缩短升级停机时间)。这些功能助力企业实现高效数据治理与分析,未来还将拓展智能化与国际化支持。
290 0
|
13天前
|
人工智能 安全 API
Dify平台集成安全护栏最佳实践
Dify平台提供低代码构建AI大模型应用的解决方案,支持云服务与私有化部署。本文介绍了在工作流和Agent中集成安全护栏的最佳实践,包括插件和扩展API两种方案。插件方式适用于工作流,一键安装实现输入输出防控;扩展API方式适用于Agent和工作流私有化部署场景,通过本地服务适配安全护栏API。文中还详细说明了操作步骤、前提条件及常见问题处理方法,帮助用户快速实现内容安全控制。
|
3月前
|
人工智能 搜索推荐 API
AI-Compass DeepSearch深度搜索生态:集成阿里ZeroSearch、字节DeerFlow、MindSearch等前沿平台,实现超越传统关键词匹配的智能信息检索革命
AI-Compass DeepSearch深度搜索生态:集成阿里ZeroSearch、字节DeerFlow、MindSearch等前沿平台,实现超越传统关键词匹配的智能信息检索革命
AI-Compass DeepSearch深度搜索生态:集成阿里ZeroSearch、字节DeerFlow、MindSearch等前沿平台,实现超越传统关键词匹配的智能信息检索革命
|
2月前
|
供应链 监控 搜索推荐
35页PPT|零售行业自助数据分析方法论:指标体系构建平台集成、会员与商品精细化运营实践
在零售行业环境剧变的背景下,传统“人找货”模式正被“货找人”取代。消费者需求日益个性化,购买路径多元化,企业亟需构建统一的指标体系,借助BI平台实现数据驱动的精细化运营。本文从指标体系构建、平台集成到会员与商品运营实践,系统梳理零售经营分析的方法论,助力企业实现敏捷决策与业务闭环。
35页PPT|零售行业自助数据分析方法论:指标体系构建平台集成、会员与商品精细化运营实践
|
3月前
|
机器学习/深度学习 人工智能 监控
CI/CD与模型监控平台集成MLOps系统实现的全面路径
MLOps是机器学习模型在生产环境中持续优化、部署和维护的关键。通过CI/CD流水线和模型监控平台的结合,可以大大提高模型开发和运维的效率,实现高效、稳定的模型服务。随着AI技术的快速发展,MLOps将在企业级AI应用中发挥越来越重要的作用。
CI/CD与模型监控平台集成MLOps系统实现的全面路径
|
7月前
|
人工智能 网络协议 Java
RuoYi AI:1人搞定AI中台!开源全栈式AI开发平台,快速集成大模型+RAG+支付等模块
RuoYi AI 是一个全栈式 AI 开发平台,支持本地 RAG 方案,集成多种大语言模型和多媒体功能,适合企业和个人开发者快速搭建个性化 AI 应用。
1621 77
RuoYi AI:1人搞定AI中台!开源全栈式AI开发平台,快速集成大模型+RAG+支付等模块
|
3月前
|
人工智能 JavaScript 安全
一文教你高效集成Qwen Code与ModelGate千万免费Toknn模型网关平台
本文详解如何高效集成Qwen Code与ModelGate模型网关平台,涵盖环境搭建、API配置、代码生成等关键步骤,助你实现智能编程与多模型管理,大幅提升AI开发效率。
|
12月前
|
前端开发 JavaScript UED
探索Python Django中的WebSocket集成:为前后端分离应用添加实时通信功能
通过在Django项目中集成Channels和WebSocket,我们能够为前后端分离的应用添加实时通信功能,实现诸如在线聊天、实时数据更新等交互式场景。这不仅增强了应用的功能性,也提升了用户体验。随着实时Web应用的日益普及,掌握Django Channels和WebSocket的集成将为开发者开启新的可能性,推动Web应用的发展迈向更高层次的实时性和交互性。
259 1
|
7月前
|
SQL 关系型数据库 MySQL
【亲测有用】数据集成平台能力演示(支持国产数据库DaMeng与KingBase)
杭州奥零数据科技有限公司成立于2023年,专注于数据中台业务,维护开源项目AllData并提供商业版解决方案。AllData提供数据集成、存储、开发、治理及BI展示等一站式服务,支持AI大模型应用,助力企业高效利用数据价值。
【亲测有用】数据集成平台能力演示(支持国产数据库DaMeng与KingBase)