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

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

相关文章
|
25天前
|
安全 定位技术 API
婚恋交友系统匹配功能 婚恋相亲软件实现定位 语音社交app红娘系统集成高德地图SDK
在婚恋交友系统中集成高德地图,可实现用户定位、导航及基于地理位置的匹配推荐等功能。具体步骤如下: 1. **注册账号**:访问高德开放平台,注册并创建应用。 2. **获取API Key**:记录API Key以备开发使用。 3. **集成SDK**:根据开发平台下载并集成高德地图SDK。 4. **配置功能**:实现定位、导航及基于位置的匹配推荐。 5. **注意事项**:保护用户隐私,确保API Key安全,定期更新地图数据,添加错误处理机制。 6. **测试优化**:完成集成后进行全面测试,并根据反馈优化功能。 通过以上步骤,提升用户体验,提供更便捷的服务。
|
1月前
|
人工智能 数据可视化 JavaScript
NodeTool:AI 工作流可视化构建器,通过拖放节点设计复杂的工作流,集成 OpenAI 等多个平台
NodeTool 是一个开源的 AI 工作流可视化构建器,通过拖放节点的方式设计复杂的工作流,无需编码即可快速原型设计和测试。它支持本地 GPU 运行 AI 模型,并与 Hugging Face、OpenAI 等平台集成,提供模型访问能力。
114 14
NodeTool:AI 工作流可视化构建器,通过拖放节点设计复杂的工作流,集成 OpenAI 等多个平台
|
17天前
|
人工智能 数据处理 C#
AI Dev Gallery:微软开源 Windows AI 模型本地运行工具包和示例库,助理开发者快速集成 AI 功能
微软推出的AI Dev Gallery,为Windows开发者提供开源AI工具包和示例库,支持本地运行AI模型,提升开发效率。
64 13
|
17天前
|
运维 监控 Cloud Native
构建深度可观测、可集成的网络智能运维平台
本文介绍了构建深度可观测、可集成的网络智能运维平台(简称NIS),旨在解决云上网络运维面临的复杂挑战。内容涵盖云网络运维的三大难题、打造云原生AIOps工具集的解决思路、可观测性对业务稳定的重要性,以及产品发布的亮点,包括流量分析NPM、网络架构巡检和自动化运维OpenAPI,助力客户实现自助运维与优化。
|
24天前
|
人工智能 自然语言处理 搜索推荐
Open Notebook:开源 AI 笔记工具,支持多种文件格式,自动转播客和生成总结,集成搜索引擎等功能
Open Notebook 是一款开源的 AI 笔记工具,支持多格式笔记管理,并能自动将笔记转换为博客或播客,适用于学术研究、教育、企业知识管理等多个场景。
100 0
Open Notebook:开源 AI 笔记工具,支持多种文件格式,自动转播客和生成总结,集成搜索引擎等功能
|
2月前
|
缓存 资源调度 JavaScript
Vue集成Excalidraw实现在线画板功能
Excalidraw是一款开源在线绘图工具,适用于白板、思维导图、原型设计等场景。支持手绘风格、多种图形元素、导出功能及多人协作,深受开发者喜爱。本文档介绍了如何在Vue项目中集成Excalidraw,包括安装依赖、配置文件修改、页面添加等步骤,帮助开发者快速上手。
210 0
Vue集成Excalidraw实现在线画板功能
|
2月前
|
传感器 前端开发 Android开发
在 Flutter 开发中,插件开发与集成至关重要,它能扩展应用功能,满足复杂业务需求
在 Flutter 开发中,插件开发与集成至关重要,它能扩展应用功能,满足复杂业务需求。本文深入探讨了插件开发的基本概念、流程、集成方法、常见类型及开发实例,如相机插件的开发步骤,同时强调了版本兼容性、性能优化等注意事项,并展望了插件开发的未来趋势。
49 2
|
2月前
|
SQL 开发框架 .NET
突破T-SQL限制:利用CLR集成扩展RDS SQL Server的功能边界
CLR集成为SQL Server提供了强大的扩展能力,突破了T-SQL的限制,极大地拓展了SQL 的应用场景,如:复杂字符串处理、高性能计算、图像处理、机器学习集成、自定义加密解密等,使开发人员能够利用 .NET Framework的丰富功能来处理复杂的数据库任务。
|
2月前
|
程序员 API 数据库
Django/Flask深度揭秘:揭秘那些让程序员爱不释手的神奇功能!
在Web开发领域,Django与Flask凭借其独特魅力和强大功能深受程序员喜爱。Django作为全能型框架,以其ORM、模板引擎和丰富的内置功能著称;Flask则以轻量级、灵活的路由系统和强大的扩展生态见长。两者各具特色,为开发者提供了高效、灵活的开发工具。
46 4
|
2月前
|
JSON Java API
springboot集成ElasticSearch使用completion实现补全功能
springboot集成ElasticSearch使用completion实现补全功能
53 1

热门文章

最新文章