文章目录
- 前言
- 一、课程列表页面
- a.后端代码
- b.前端代码
- 二、课程详情页面
- a. 视频播放功能的集成
- 1.获取上传视频的链接地址
- 2.集成在前端页面中
- 1>使用vue-alipayer视频播放组件
- 2>使用video标签
- b. 页面主要内容展示
- 1.后端代码
- 1>分析表
- 2>核心逻辑
- 2.前端代码
- 3.效果图
前言
随着数字化教育的兴起,构建一个高效、用户友好的线上教育平台至关重要。本文将探讨如何使用Django与Vue.js 3结合,实现一个包含课程列表和课程详情页(含视频播放功能)的线上教育平台部分。本文主要介绍了如何设计数据库模型、处理数据查询、构建动态前端界面,并集成视频播放功能,为用户带来流畅的学习体验。
一、课程列表页面
获取所有一级分类,获取所有二级分类,获取所有课程(课程分页处理),点击方向和分类时获取此方向或者此分类下的数据信息
页面展示:
a.后端代码
url配置信息:
path('nav/cates/', CategoryView.as_view()), #课程列表页面 /project-方向/一级分类 ----- 侧边栏-获取一二级分类 -path('nav/category/', CateView.as_view()), #课程列表页面 /project-二级分类#课程列表页面 / projectpath('courseSearch/', CourseSearch.as_view()), # /project页面--搜索课程---
获取方向、分类及课程信息:
# 2.获取一、二级分类
class CategoryView(APIView):def get(self, request):# 查询所有一级分类:parent is null# query_setcategories = CategoryModel.objects.filter(is_delete=0,parent__id__isnull=True) #query_setclist = [] #侧边栏 二级分类显示几个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-paginationstyle="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视频播放组件为例实现视频播放功能
- 七牛云官网地址:https://www.qiniu.com/
- vue-alipayer视频播放组件地址:https://github.com/liho98/vue-aliplayer-v2
- 演示效果:https://player.alicdn.com/aliplayer/index.html
1.获取上传视频的链接地址
具体操作步骤如下:
- 1.七牛云注册登录:https://www.qiniu.com/
- 2.点击对象存储:
- 3.创建存储空间:
- 4.创建成功:
- 5.上传一段视频用于在课程详情页面展示:
- 6.视频上传成功:
- 7.查看文件详情,可获得文件链接:
2.集成在前端页面中
1>使用vue-alipayer视频播放组件
<AliPlayerV3ref="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-->jsontest_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-->strr.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 + chapterschapt = 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"><!-- 视频播放器 --><AliPlayerV3ref="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.效果图
- 详情介绍
- 课程章节
- 用户评论
- 常见问题