@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.效果图
- 详情介绍
- 课程章节
- 用户评论
- 常见问题