文章目录
我的订单
打通头部子组件的链接,代码:
Header.vue
<div class="login-box login-box1 full-left">
<router-link to="">学习中心</router-link>
<el-menu width="200" class="member el-menu-demo" mode="horizontal">
<el-submenu index="2">
<template slot="title"><router-link to=""><img src="/static/image/logo@2x.png" alt=""></router-link></template>
<el-menu-item index="2-1">我的账户</el-menu-item>
<el-menu-item index="2-2"><router-link to="/user/order">我的订单</router-link></el-menu-item>
<el-menu-item index="2-3">我的优惠卷</el-menu-item>
<el-menu-item index="2-3"><span @click="logoutHander">退出登录</span></el-menu-item>
</el-submenu>
</el-menu>
</div>
前端显示我的订单页面,代码:
user/Order.vue
<template>
<div class="user-order">
<Header/>
<div class="main">
<div class="banner"></div>
<div class="profile">
<div class="profile-info">
<div class="avatar"><img class="newImg" width="100%" alt="" src="../../../static/image/logo@2x.png"></div>
<span class="user-name">Mixtea</span>
<span class="user-job">深圳市 | 程序员</span>
</div>
<ul class="my-item">
<li>我的账户</li>
<li class="active">我的订单</li>
<li>个人资料</li>
<li>账号安全</li>
</ul>
</div>
<div class="user-data">
<ul class="nav">
<li class="order-info">订单</li>
<li class="course-expire">有效期</li>
<li class="course-price">课程价格</li>
<li class="real-price">实付金额</li>
<li class="order-status">交易状态</li>
<li class="order-do">交易操作</li>
</ul>
<div class="my-order-item">
<div class="user-data-header">
<span class="order-time">2019-04-02 10:27:49</span>
<span class="order-num">订单号:
<span class="my-older-number">20190402102749606</span>
</span>
</div>
<ul class="nav user-data-list">
<li class="order-info">
<img src="../../../static/image/course-cover.jpeg" alt="">
<div class="order-info-title">
<p class="course-title">Pycharm使用秘籍</p>
<p class="price-service">限时免费</p>
</div>
</li>
<li class="course-expire">永久有效</li>
<li class="course-price">977.00</li>
<li class="real-price">577.00</li>
<li class="order-status">交易成功</li>
<li class="order-do">
<span class="btn btn2">去学习</span>
</li>
</ul>
</div>
</div>
</div>
<Footer/>
</div>
</template>
<script>
import Header from "../common/Header"
import Footer from "../common/Footer"
export default{
name:"MyOrder",
data(){
return {
};
},
created(){
this.check_login();
},
methods:{
check_login(){
// 检查当前访问者是否登录了!
let token = localStorage.user_token || sessionStorage.user_token;
if( !token ){
this.$alert("对不起,您尚未登录,请登录以后再进行购物车").then(()=>{
this.$router.push("/user/login");
});
return false; // 阻止代码往下执行
}
return token;
},
get_user_order(){
// 获取当前登录用户的所有订单
},
},
components:{
Header,
Footer,
}
}
</script>
<style scoped>
.main .banner{
width: 100%;
height: 324px;
background: url(../../../static/image/my_bkging.0648ebe.png) no-repeat;
background-size: cover;
z-index: 1;
}
.profile{
width: 1200px;
margin: 0 auto;
}
.profile-info{
text-align: center;
margin-top: -80px;
}
.avatar{
width: 120px;
height: 120px;
border-radius: 60px;
overflow: hidden;
margin: 0 auto;
}
.user-name{
display: block;
font-size: 24px;
color: #4a4a4a;
margin-top: 14px;
}
.user-job{
display: block;
font-size: 11px;
color: #9b9b9b;
}
.my-item{
list-style: none;
line-height: 1.42857143;
color: #333;
width: 474px;
height: 31px;
display: -ms-flexbox;
display: flex;
cursor: pointer;
margin: 41px auto 0;
-ms-flex-pack: justify;
justify-content: space-between;
}
.my-item .active{
border-bottom: 1px solid #000;
}
.user-data{
width: 1200px;
height: auto;
margin: 0 auto;
padding-top: 30px;
border-top: 1px solid #e8e8e8;
margin-bottom: 63px;
}
.nav{
width: 100%;
height: 60px;
background: #e9e9e9;
display: -ms-flexbox;
display: flex;
-ms-flex-align: center;
align-items: center;
}
.nav li{
margin-left: 20px;
margin-right: 28px;
height: 60px;
line-height: 60px;
list-style: none;
font-size: 13px;
color: #333;
border-bottom: 1px solid #e9e9e9;
width: 160px;
}
.nav .order-info{ width: 325px; }
.nav .course-expire{ width: 60px; }
.nav .course-price{ width: 130px; }
.user-data-header{
display: flex;
height: 44px;
color: #4a4a4a;
font-size: 14px;
background: #f3f3f3;
-ms-flex-align: center;
align-items: center;
}
.order-time{
font-size: 12px;
display: inline-block;
margin-left: 20px;
}
.order-num{
font-size: 12px;
display: inline-block;
margin-left: 29px;
}
.user-data-list{
height: 100%;
display: flex;
}
.user-data-list{
background: none;
}
.user-data-list li{
height: 60px;
line-height: 60px;
}
.user-data-list .order-info{
display: flex;
align-items: center;
margin-right: 28px;
}
.user-data-list .order-info img{
max-width: 100px;
max-height: 75px;
margin-right: 22px;
}
.course-title{
width: 203px;
font-size: 13px;
color: #333;
line-height: 20px;
margin-top: -10px;
}
.order-info-title .price-service{
line-height: 18px;
}
.price-service{
font-size: 12px;
color: #fa6240;
padding: 0 5px;
border: 1px solid #fa6240;
border-radius: 4px;
margin-top: 4px;
position: absolute;
}
.order-info-title{
margin-top: -10px;
}
.user-data-list .course-expire{
font-size: 12px;
color: #ff5502;
width: 60px;
text-align: center;
}
.btn {
width: 100px;
height: 32px;
font-size: 14px;
color: #fff;
background: #ffc210;
border-radius: 4px;
border: none;
outline: none;
transition: all .25s ease;
display: inline-block;
line-height: 32px;
text-align: center;
cursor: pointer;
}
</style>
路由注册:
router/index.js
import Vue from "vue"
import Router from "vue-router"
// 这里导入可以让让用户访问的组件
// vue 中提供了@符号,表示src路径
import Home from "@/components/Home"
import Login from "@/components/Login"
import Register from "@/components/Register"
import Course from "@/components/Course"
import Detail from "@/components/Detail"
import Cart from "@/components/Cart"
import Order from "@/components/Order"
import Success from "@/components/Success"
import UserOrder from "@/components/user/Order"
Vue.use(Router);
export default new Router({
// 设置路由模式为‘history’,去掉默认的#
mode: "history",
routes:[
// 路由列表
{
path:"/",
name:"Home",
component:Home
},
{
path:"/home",
name:"Home",
component:Home
},
{
path:"/user/login",
name:"Login",
component: Login
},
{
path:"/user/register",
name:"Register",
component: Register
},
{
path:"/course",
name:"Course",
component: Course
},
{
path:"/course/:course",
name:"Detail",
component: Detail
},
{
path:"/cart",
name:"Cart",
component: Cart
},
{
path:"/order",
name:"Order",
component: Order
},
{
path:"/pay/result",
name:"Success",
component: Success
},
{
path:"/user/order",
name:"UserOrder",
component: UserOrder
}
]
});
后端提供查询当前登录用户的订单列表信息
orders/models.py
,模型新增返回订单状态的文本格式
from django.db import models
from luffyapi.utils.models import BaseModel
from users.models import User
from courses.models import Course
from django.conf import settings
from courses.models import CourseExpire
class Order(BaseModel):
"""订单模型"""
...
def status(self):
"""返回订单状态的文本提示"""
return self.status_choices[self.order_status][1]
def course_list(self):
"""订单详情的课程列表"""
order_detail = self.order_courses.all()
data = []
for item in order_detail:
# 获取本地订单中用户购买的课程有效期
try:
course_expire = CourseExpire.objects.get(expire_time=item.expire,course=item.course)
expire = course_expire.expire_text
except:
if item.expire == 0:
expire = "永久有效"
else:
raise CourseExpire.DoesNotExist()
data.append({
"id": item.course.id,
"course_name": item.course.name,
"course_img": settings.DOMAIL_IMAGE_URL[:-1]+item.course.course_img.url,
"expire": expire,
"price": item.price,
"real_price": item.real_price,
"discount_name": item.discount_name
})
return data
class OrderDetail(BaseModel):
"""订单详情"""
order = models.ForeignKey(Order, related_name='order_courses', on_delete=models.CASCADE, verbose_name="订单")
course = models.ForeignKey(Course, related_name='course_orders', on_delete=models.CASCADE, verbose_name="课程")
expire = models.IntegerField(default='0', verbose_name="有效期周期")
price = models.DecimalField(max_digits=6, decimal_places=2, verbose_name="课程原价")
real_price = models.DecimalField(max_digits=6, decimal_places=2, verbose_name="课程实价")
discount_name = models.CharField(max_length=120,null=True, default="",blank="", verbose_name="优惠类型")
class Meta:
db_table="ly_order_detail"
verbose_name= "订单详情"
verbose_name_plural= "订单详情"
def __str__(self):
return "%s" % (self.course.name)
users/serializers.py
,序列化器,代码:
from orders.models import Order
class UserOrderModelSerializer(serializers.ModelSerializer):
class Meta:
model = Order
fields = ["id","created_time","order_number","status","order_status","pay_time","course_list"]
users/views.py
,视图代码:
from rest_framework.generics import ListAPIView
from orders.models import Order
from .seriazliers import UserOrderModelSerializer
from rest_framework.permissions import IsAuthenticated
class UserOrderAPIView(ListAPIView):
"""用户的订单列表"""
queryset = Order.objects.filter()
serializer_class = UserOrderModelSerializer
permission_classes = [IsAuthenticated]
def list(self, request, *args, **kwargs):
# 重写列表查询方法,在数据查询的时候新增当前用户的过滤条件
queryset = self.filter_queryset(self.get_queryset().filter(user=request.user))
page = self.paginate_queryset(queryset)
if page is not None:
serializer = self.get_serializer(page, many=True)
return self.get_paginated_response(serializer.data)
serializer = self.get_serializer(queryset, many=True)
return Response(serializer.data)
users/urls.py
,路由代码:
path("orders/", views.UserOrderAPIView.as_view() ),
前端请求获取当前登录用户的订单信息
user/Order.vue
<template>
<div class="user-order">
<Header/>
<div class="main">
<div class="banner"></div>
<div class="profile">
<div class="profile-info">
<div class="avatar"><img class="newImg" width="100%" alt="" src="../../../static/image/logo@2x.png"></div>
<span class="user-name">{{user_name}}</span>
<span class="user-job">深圳市 | 程序员</span>
</div>
<ul class="my-item">
<li>我的账户</li>
<li class="active">我的订单</li>
<li>个人资料</li>
<li>账号安全</li>
</ul>
</div>
<div class="user-data">
<ul class="nav">
<li class="order-info">订单</li>
<li class="course-expire">有效期</li>
<li class="course-price">课程价格</li>
<li class="real-price">实付金额</li>
<li class="order-status">交易状态</li>
<li class="order-do">交易操作</li>
</ul>
<div class="my-order-item" v-for="order in order_list">
<div class="user-data-header">
<span class="order-time">{{order.created_time|timeformat}}</span>
<span class="order-num">订单号:
<span class="my-older-number">{{order.order_number}}</span>
</span>
</div>
<ul class="nav user-data-list" v-for="course in order.course_list">
<li class="order-info">
<img :src="course.course_img" :alt="course.course_name">
<div class="order-info-title">
<p class="course-title">{{course.course_name}}</p>
<p class="price-service" v-if="course.discount_name">{{course.discount_name}}</p>
</div>
</li>
<li class="course-expire">{{course.expire_text}}</li>
<li class="course-price">{{course.price.toFixed(2)}}</li>
<li class="real-price">{{course.real_price.toFixed(2)}}</li>
<li class="order-status">{{order.status}}</li>
<li class="order-do">
<span class="btn btn2" v-if="order.order_status==1">去学习</span>
<span class="btn btn2" v-if="order.order_status==0" @click="get_alipay_payment_url(order.order_number)">去支付</span>
</li>
</ul>
</div>
</div>
</div>
<Footer/>
</div>
</template>
<script>
import Header from "../common/Header"
import Footer from "../common/Footer"
export default{
name:"MyOrder",
data(){
return {
user_name: localStorage.user_name || sessionStorage.user_name,
order_list:[]
};
},
created(){
this.check_login();
this.get_user_order();
},
methods:{
check_login(){
// 检查当前访问者是否登录了!
let token = localStorage.user_token || sessionStorage.user_token;
if( !token ){
this.$alert("对不起,您尚未登录,请登录以后再进行购物车").then(()=>{
this.$router.push("/user/login");
});
return false; // 阻止代码往下执行
}
return token;
},
get_user_order(){
// 获取当前登录用户的所有订单
this.$axios.get(`${this.$settings.Host}/user/orders/`,{
headers:{
"Authorization": "jwt " + this.check_login(),
}
}).then(response=>{
this.order_list = response.data;
console.log(this.order_list);
}).catch(error=>{
console.log(error.response);
});
},
get_alipay_payment_url(order_number){
// 获取支付宝的支付地址
this.$axios.post(`${this.$settings.Host}/payments/${order_number}/alipay/`).then(response=>{
console.log(response.data.pay_url);
// 页面跳转
location.href=response.data.pay_url;
}).catch(error=>{
console.log(error.response);
})
}
},
filters:{
timeformat(time){
// 时间格式化
// 2019/04/02 10:27
let current_obj = new Date(time);
// 年份
let Y = current_obj.getFullYear();
// 月份
let m = current_obj.getMonth()+1;
m = m<10?"0"+m:m;
// 日期
let d = current_obj.getDate();
d = d<10?"0"+d:d;
// 小时
let H = current_obj.getHours();
H = H<10?"0"+H:H;
// 分钟
let i = current_obj.getMinutes();
i = i<10?"0"+i:i;
// 秒
let s = current_obj.getSeconds();
s = s<10?"0"+s:s;
return `${Y}/${m}/${d} ${H}:${i}:${s}`;
}
},
components:{
Header,
Footer,
}
}
</script>
订单状态显示分析
根据订单状态显示:
1. 如果未支付[order.order_stauts=0],则显示"去支付"按钮
2. 如果已支付[order.order_stauts=1],则显示"去学习"按钮
3. 如果未支付,并超过指定时间[12个小时],则显示"超时取消" [Celery / Django-crontab 定时任务 ]
用户下单在12小时以后自动判断订单状态如果是0,则直接改成3
定时任务[crontab],主要是依靠:操作系统的定时计划或者第三方软件的定时执行
定时任务的常见场景:
1. 订单超时
2. 生日邮件[例如,每天凌晨检查当天有没有用户生日,有则发送一份祝福邮件]
3. 财务统计[例如,每个月的1号,把当月的订单进行统计,生成一个财务记录,保存到数据库中]
4. 页面缓存[例如,把首页设置为每隔5分钟生成一次缓存]
使用Celery的定时任务来完成订单超时功能
Celery官方文档中关于定时任务使用的说明:
http://docs.celeryproject.org/en/latest/userguide/periodic-tasks.html
在实现定时任务之前,我们需要先简单使用一下。
我们需要新增一个任务目录,例如order
在main.py
中,注册任务目录【注意,接下来后面我们使用django
的模型处理,所以必须对django的配置进行引入】
import os
from celery import Celery
# 1. 创建示例对象
app = Celery("luffy")
# 2. 加载配置
app.config_from_object("celery_tasks.config")
# 3. 注册任务[自动搜索并加载任务]
# 参数必须必须是一个列表,里面的每一个任务都是任务的路径名称
# app.autodiscover_tasks(["任务1","任务2"])
app.autodiscover_tasks(["celery_tasks.sms","celery_tasks.order"])
# 4. 在终端下面运行celery命令启动celery
# celery -A 主程序 worker --loglevel=info
# celery -A celery_tasks.main worker --loglevel=info
接下来,在order
任务目录下, 创建固定名字的任务文件tasks.py
,代码:
from celery_tasks.main import app
@app.task(name="check_order")
def check_order():
print("检查订单是否过期!!!")
接下来,我们需要把这个任务设置定时任务,所以需要借助Celery本身提供的Crontab
模块。
在配置文件中,对定时任务进行注册:
config.py
# 任务队列的链接地址
broker_url = 'redis://127.0.0.1:6379/15'
# 结果队列的链接地址
result_backend = 'redis://127.0.0.1:6379/14'
from celery.schedules import crontab
from .main import app
# 定时任务的调度列表,用于注册定时任务
app.conf.beat_schedule = {
# Executes every Monday morning at 7:30 a.m.
'check_order_outtime': {
# 本次调度的任务
'task': 'check_order', # 这里的任务名称必须先到main.py中注册
# 定时任务的调度周期
# 'schedule': crontab(minute=0, hour=0), # 每周凌晨00:00
'schedule': crontab(), # 每分钟
# 'args': (16, 16), # 注意:任务就是一个函数,所以如果有参数则需要传递
},
}
接下来,我们就可以重启Celery并启用Celery的定时任务调度器
先在终端下,运行celery的定时任务程序,以下命令:
celery -A celery_tasks.main beat # ycelery.main 是celery的主应用文件
然后再新建一个终端,运行以下命令,上面的命令必须先指定:
celery -A celery_tasks.main worker --loglevel=info
注意,使用的时候,如果有时区必须先配置好系统时区。
经过上面的测试以后,我们接下来只需改造上面的任务函数,用于判断修改订单是否超时。
要完成订单的任务功能,如果需要调用django框架的模型操作,那么必须针对django框架进行配置加载和初始化。
main.py
,代码:
import os
from celery import Celery
# 1. 创建示例对象
app = Celery("luffy")
# 把celery和django进行组合,识别和加载django的配置文件
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'luffyapi.settings.dev')
# 在当前clery中启动django框架,对django框架进行进行初始化
import django
django.setup()
# 2. 加载配置
app.config_from_object("celery_tasks.config")
# 3. 注册任务[自动搜索并加载任务]
# 参数必须必须是一个列表,里面的每一个任务都是任务的路径名称
# app.autodiscover_tasks(["任务1","任务2"])
app.autodiscover_tasks(["celery_tasks.sms","celery_tasks.order"])
# 4. 在终端下面运行celery命令启动celery
# celery -A 主程序 worker --loglevel=info
# celery -A celery_tasks.main worker --loglevel=info
注意,因为在django中是有时区配置的,所以,我们在django框架配置中也要修改时区配置。
任务代码tasks.py
的实现:
from celery_tasks.main import app
from orders.models import Order
from datetime import datetime
from django.conf import settings
@app.task(name="check_order")
def check_order():
# 查询出所有已经超时的订单
# 超时条件: 当前时间 > (订单生成时间 + 超时时间) =====>>>> (当前时间 - 超时时间) > 订单生成时间
now = datetime.now().timestamp()
timeout_number = now - settings.ORDER_TIMEOUT
timeout = datetime.fromtimestamp(timeout_number)
timeout_order_list = Order.objects.filter(order_status=0, created_time__lte=timeout)
for order in timeout_order_list:
order.order_status = 3
order.save()
配置文件,settings/dev.py
,代码:
# 设置订单超时超时的时间[单位: 秒]
ORDER_TIMEOUT = 12 * 60 * 60
重新启动celery的定时任务模块和celery的主应用程序。
视频播放
项目中有两种视频:收费视频[需要加密]和免费视频
使用保利威云视频服务来对视频进行加密
官方网址: http://www.polyv.net/vod/
注意:
开发时通过
免费试用
注册体验版账号【测试账号的测试有效期是一周】公司使用酷播尊享版
开发文档地址: http://dev.polyv.net/2017/videoproduct/v-playerapi/html5player/html5-docs/
要开发播放保利威的加密视频功能,需要在用户中心->设置->API接口和加密设置
.
http://my.polyv.net/secure/setting/api
配置视频上传加密.
上传视频并记录视频的VID
后端获取保利威的视频播放授权token,提供接口api给前端
参考文档:http://dev.polyv.net/2019/videoproduct/v-api/v-api-play/create-playsafe-token/
根据官方文档的案例,已经有其他人开源了,针对polvy的token生成的python版本了,我们可以直接拿来使用.
在libs下
创建polyv.py
,编写token生成工具函数
from django.conf import settings
import time
import requests
import hashlib
class PolyvPlayer(object):
def __init__(self,userId,secretkey,tokenUrl):
"""初始化,提供用户id和秘钥"""
self.userId = userId
self.secretKey = secretkey
self.tokenUrl = tokenUrl
def tomd5(self, value):
"""取md5值"""
return hashlib.md5(value.encode()).hexdigest()
# 获取视频数据的token
def get_video_token(self, videoId, viewerIp, viewerId=None, viewerName='', extraParams='HTML5'):
"""
:param videoId: 视频id
:param viewerId: 看视频用户id
:param viewerIp: 看视频用户ip
:param viewerName: 看视频用户昵称
:param extraParams: 扩展参数
:param sign: 加密的sign
:return: 返回点播的视频的token
"""
ts = int(time.time() * 1000) # 时间戳
plain = {
"userId": self.userId,
'videoId': videoId,
'ts': ts,
'viewerId': viewerId,
'viewerIp': viewerIp,
'viewerName': viewerName,
'extraParams': extraParams
}
# 按照ASCKII升序 key + value + key + value... + value 拼接
plain_sorted = {}
key_temp = sorted(plain)
for key in key_temp:
plain_sorted[key] = plain[key]
print(plain_sorted)
plain_string = ''
for k, v in plain_sorted.items():
plain_string += str(k) + str(v)
print(plain_string)
# 首尾拼接上秘钥
sign_data = self.secretKey + plain_string + self.secretKey
# 取sign_data的md5的大写
sign = self.tomd5(sign_data).upper()
# 新的带有sign的字典
plain.update({'sign': sign})
# python 提供的发送http请求的模块
result = requests.post(
url=self.tokenUrl,
headers={"Content-type": "application/x-www-form-urlencoded"},
data=plain
).json()
token = {} if isinstance(result, str) else result.get("data", {})
return token
配置文件settings/dev.py
,代码:
# 保利威视频加密服务
POLYV_CONFIG = {
"userId":"62d***3f",
"secretkey":"h6*****RMU",
"tokenUrl":"https://hls.videocc.net/service/v1/token",
}
courses/views.py
,视图代码:
from luffyapi.libs.polyv import PolyvPlayer
from django.conf import settings
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.permissions import IsAuthenticated
class PolyvAPIView(APIView):
permission_classes = [IsAuthenticated]
def get(self, request):
"""获取保利威云视频加密播放的token"""
"""接受客户端的请求参数"""
vid = request.query_params.get("vid") # 视频播放ID
remote_addr = request.META.get("REMOTE_ADDR") # 用户的IP
user_id = request.user.id # 用户ID
user_name = request.user.username # 用户名
polyv = PolyvPlayer(
settings.POLYV_CONFIG["userId"],
settings.POLYV_CONFIG["secretkey"],
settings.POLYV_CONFIG["tokenUrl"],
)
data = polyv.get_video_token(vid,remote_addr,user_id,user_name)
return Response(data)
courses/urls.py
,路由代码:
path(r"polyv/token/",views.PolyvAPIView.as_view()),
客户端请求token并播放视频
在 vue项目的入口文件index.html
中加载保利威视频播放器的js
核心类库
<script src='https://player.polyv.net/script/polyvplayer.min.js'></script>
创建视频播放页面的组件Player.vue
,代码:
<template>
<div class="player">
<div id="player"></div>
</div>
</template>
<script>
export default {
name:"Player",
data () {
return {
}
},
methods: {
},
computed: {
}
}
</script>
<style scoped>
</style>
前端路由,router/index.js
代码:
{
name:"Player",
path:"/player",
component: Player,
},
引入保利威前端HTML5视频播放器代码,Player.vue
:
<template>
<div class="player">
<div id="player"></div>
</div>
</template>
<script>
export default {
name:"Player",
data () {
return {
}
},
methods: {
check_login(){
// 检查当前访问者是否登录了!
let token = localStorage.user_token || sessionStorage.user_token;
if( !token ){
this.$alert("对不起,您尚未登录,请登录以后再进行购物车").then(()=>{
this.$router.push("/user/login");
});
return false; // 阻止代码往下执行
}
return token;
},
},
mounted(){
// 验证用户是否登录
let token = this.check_login();
let user_name = localStorage.user_name || sessionStorage.user_name;
let _this = this;
let vid = "d6f2d2d505673e0a75cef00f8d5284f6_d";
var player = polyvObject('#player').videoPlayer({
wrap: '#player',
width: document.documentElement.clientWidth-260, // 页面宽度
height: document.documentElement.clientHeight, // 页面高度
forceH5: true,
vid: vid,
code: user_name, // 一般是用户昵称
// 视频加密播放的配置
playsafe: function (vid, next) { // 向后端发送请求获取加密的token
_this.$axios.get(`${_this.$settings.Host}/course/polyv/token/`,{
params:{
vid: vid,
},
headers:{
"Authorization":"jwt " + token,
}
}).then(function (response) {
console.log(response);
next(response.data.token);
})
}
});
},
computed: {
}
}
</script>
<style scoped>
</style>
完善课程详情页的视频内容显示
1. 课程详情中有封面视频播放,所以我们需要在后端的课程模型中新增一个字段course_video
2. 在序列化器中返回的内容增加course_video
3. 在课程详情页组件中显示封面视频或者封面图片
课程详情中有封面视频播放,所以我们需要在后端的课程模型中新增一个字段course_video
courses/models.py
,代码:
class Course(BaseModel):
"""
专题课程
"""
course_type = (
(0, '付费'),
(1, 'VIP专享'),
(2, '学位课程')
)
level_choices = (
(0, '初级'),
(1, '中级'),
(2, '高级'),
)
status_choices = (
(0, '上线'),
(1, '下线'),
(2, '预上线'),
)
name = models.CharField(max_length=128, verbose_name="课程名称")
course_img = models.ImageField(upload_to="course", max_length=255, verbose_name="封面图片", blank=True, null=True)
course_video = models.FileField(upload_to="course", max_length=255, verbose_name="封面视频", blank=True, null=True)
# ....
执行数据迁移,
python manage.py makemigrations
python manage.py migrate
在序列化器中返回的内容增加course_video
class CourseRetrieveSerializer(serializers.ModelSerializer):
# 课程详情的序列化器
teacher = TeacherSerializer()
class Meta:
model = Course
fields = ["id","name","course_img","course_video","students","lessons","pub_lessons","price","teacher","brief_text","level_name","active_time","discount_type","real_price",]
在课程详情页组件中显示封面视频或者封面图片
<template>
<div class="detail">
<Header/>
<div class="main">
<div class="course-info">
<div class="wrap-left">
<videoPlayer v-if="course_info.course_video" class="video-player vjs-custom-skin"
ref="videoPlayer"
:playsinline="true"
:options="playerOptions"
@play="onPlayerPlay($event)"
@pause="onPlayerPause($event)">
</videoPlayer>
<img v-else class="course_img" :src="course_info.course_img" alt="">
</div>
<div class="wrap-right">
<h3 class="course-name">{{course_info.name}}</h3>
<p class="data">{{course_info.students}}人在学 课程总时长:{{course_info.lessons}}课时/{{course_info.pub_lessons}}小时 难度:{{course_info.level_name}}</p>
<div v-if="course_info.active_time>0">
<div class="sale-time">
<p class="sale-type">{{course_info.discount_type}}</p>
<p class="expire">距离结束:仅剩{{day}}天 {{hour}}小时 {{minute}}分 <span class="second">{{second}}</span> 秒</p>
</div>
<p class="course-price">
<span>活动价</span>
<span class="discount">¥{{course_info.real_price}}</span>
<span class="original">¥{{course_info.price}}</span>
</p>
</div>
<div v-else class="sale-time">
<p class="sale-type">价格 <span class="original_price">¥{{course_info.price}}</span></p>
<p class="expire"></p>
</div>
<div class="buy">
<div class="buy-btn">
<button class="buy-now">立即购买</button>
<button class="free">免费试学</button>
</div>
<div class="add-cart" @click="add_cart(course_info.id)"><img src="/static/image/cart-yellow.svg" alt="">加入购物车</div>
</div>
</div>
</div>
<div class="course-tab">
<ul class="tab-list">
<li :class="tabIndex==1?'active':''" @click="tabIndex=1">详情介绍</li>
<li :class="tabIndex==2?'active':''" @click="tabIndex=2">课程章节 <span :class="tabIndex!=2?'free':''">(试学)</span></li>
<li :class="tabIndex==3?'active':''" @click="tabIndex=3">用户评论 (42)</li>
<li :class="tabIndex==4?'active':''" @click="tabIndex=4">常见问题</li>
</ul>
</div>
<div class="course-content">
<div class="course-tab-list">
<div class="tab-item" v-if="tabIndex==1">
<div class="course-brief" v-html="course_info.brief_text"></div>
</div>
<div class="tab-item" v-if="tabIndex==2">
<div class="tab-item-title">
<p class="chapter">课程章节</p>
<p class="chapter-length">共{{course_chapters.length}}章 {{course_info.lessons}}个课时</p>
</div>
<div class="chapter-item" v-for="chapter in course_chapters">
<p class="chapter-title"><img src="/static/image/1.svg" alt="">第{{chapter.chapter}}章·{{chapter.name}}</p>
<ul class="lesson-list">
<li class="lesson-item" v-for="lesson in chapter.coursesections">
<p class="name"><span class="index">{{chapter.chapter}}-{{lesson.id}}</span> {{lesson.name}}<span class="free" v-if="lesson.free_trail">免费</span></p>
<p class="time">{{lesson.duration}} <img src="/static/image/chapter-player.svg"></p>
<button class="try" v-if="lesson.free_trail"><router-link :to="{path: '/player',query:{'vid':lesson.section_link}}">立即试学</router-link></button>
<button class="try" v-else>立即购买</button>
</li>
</ul>
</div>
</div>
<div class="tab-item" v-if="tabIndex==3">
用户评论
</div>
<div class="tab-item" v-if="tabIndex==4">
常见问题
</div>
</div>
<div class="course-side">
<div class="teacher-info">
<h4 class="side-title"><span>授课老师</span></h4>
<div class="teacher-content">
<div class="cont1">
<img :src="course_info.teacher.image">
<div class="name">
<p class="teacher-name">{{course_info.teacher.name}} {{course_info.teacher.title}}</p>
<p class="teacher-title">{{course_info.teacher.signature}}</p>
</div>
</div>
<p class="narrative" >{{course_info.teacher.brief}}</p>
</div>
</div>
</div>
</div>
</div>
<Footer/>
</div>
</template>
<script>
import Header from "./common/Header"
import Footer from "./common/Footer"
// 加载组件
import {videoPlayer} from 'vue-video-player';
export default {
name: "Detail",
data(){
return {
tabIndex:2, // 当前选项卡显示的下标
course_id: 0, // 当前课程信息的ID
course_info: {
teacher:{},
}, // 课程信息
course_chapters:[], // 课程的章节课时列表
playerOptions: {
playbackRates: [0.7, 1.0, 1.5, 2.0], // 播放速度
autoplay: false, //如果true,则自动播放
muted: false, // 默认情况下将会消除任何音频。
loop: false, // 循环播放
preload: 'auto', // 建议浏览器在<video>加载元素后是否应该开始下载视频数据。auto浏览器选择最佳行为,立即开始加载视频(如果浏览器支持)
language: 'zh-CN',
aspectRatio: '16:9', // 将播放器置于流畅模式,并在计算播放器的动态大小时使用该值。值应该代表一个比例 - 用冒号分隔的两个数字(例如"16:9"或"4:3")
fluid: true, // 当true时,Video.js player将拥有流体大小。换句话说,它将按比例缩放以适应其容器。
sources: [{ // 播放资源和资源格式
type: "video/mp4",
src: "" //你的视频地址(必填)
}],
poster: "", //视频封面图
width: document.documentElement.clientWidth, // 默认视频全屏时的最大宽度
notSupportedMessage: '此视频暂无法播放,请稍后再试', //允许覆盖Video.js无法播放媒体源时显示的默认信息。
}
}
},
computed:{
day(){
let day = parseInt( this.course_info.active_time/ (24*3600));
if(day < 10){
return '0'+day;
}else{
return day;
}
},
hour(){
let rest = parseInt( this.course_info.active_time % (24*3600) );
let hours = parseInt(rest/3600);
if(hours < 10){
return '0'+hours;
}else{
return hours;
}
},
minute(){
let rest = parseInt( this.course_info.active_time % 3600 );
let minute = parseInt(rest/60);
if(minute < 10){
return '0'+minute;
}else{
return minute;
}
},
second(){
let second = this.course_info.active_time % 60;
if(second < 10){
return '0'+second;
}else{
return second;
}
}
},
created(){
this.get_course_id();
this.get_course_data();
this.get_chapter();
},
methods: {
onPlayerPlay(event){
// 当视频播放时,执行的方法
alert("关闭广告")
},
onPlayerPause(event){
// 当视频暂停播放时,执行的方法
alert("显示广告");
},
get_course_id(){
// 获取地址栏上面的课程ID
this.course_id = this.$route.params.course;
if( this.course_id < 1 ){
let _this = this;
_this.$alert("对不起,当前视频不存在!","警告",{
callback(){
_this.$router.go(-1);
}
});
}
},
get_course_data(){
// ajax请求课程信息
this.$axios.get(`${this.$settings.Host}/courses/${this.course_id}/`).then(response=>{
// console.log(response.data);
this.course_info = response.data;
this.playerOptions.poster = response.data.course_img;
// 在服务端中新增一个模型字段 course_video,如果有视频,则显示到播放器中,如果没有则显示一张封面图片
if(response.data.course_video){
// this.playerOptions.sources[0].src = "http://img.ksbbs.com/asset/Mon_1703/05cacb4e02f9d9e.mp4";
this.playerOptions.sources[0].src = response.data.course_video;
}
// 在获取到剩余的活动时间以后,就要进入倒计时
let timer = setInterval(()=>{
if(this.course_info.active_time<1){
clearInterval(timer);
}else{
--this.course_info.active_time;
}
},1000);
}).catch(response=>{
this.$message({
message:"对不起,访问页面出错!请联系客服工作人员!"
});
})
},
get_chapter(){
// 获取当前课程对应的章节课时信息
this.$axios.get(`${this.$settings.Host}/courses/chapters/`,{
params:{
"course": this.course_id,
}
}).then(response=>{
this.course_chapters = response.data;
}).catch(error=>{
console.log(error.response);
})
},
add_cart(course_id){
// 添加商品到购物车
// 验证用户登录状态,如果登录了则可以添加商品到购物车,如果没有登录则跳转到登录界面,登录完成以后,才能添加商品到购物车
let token = localStorage.token || sessionStorage.token;
if( !token ){
this.$confirm("对不起,您尚未登录,请登录以后再进行购物车").then(()=>{
this.$router.push("/login/");
});
return false; // 阻止代码往下执行
}
// 添加商品到购物车,因为购物车接口必须用户是登录的,所以我们要在请求头中设置 jwttoken
this.$axios.post(`${this.$settings.Host}/cart/`,{
"course_id": course_id,
},{
headers:{
"Authorization":"jwt " + token,
}
}).then(response=>{
this.$message({
message:response.data.message,
});
// 购物车中的商品数量
let total = response.data.total;
this.$store.commit("change_total",total)
}).catch(error=>{
this.$message({
message:error.response.data
})
})
}
},
components:{
Header,
Footer,
videoPlayer, // 注册组件
}
}
</script>
<style scoped>
.main{
background: #fff;
padding-top: 30px;
}
.course-info{
width: 1200px;
margin: 0 auto;
overflow: hidden;
}
.wrap-left{
float: left;
width: 690px;
height: 388px;
background-color: #000;
}
.wrap-left .course_img{
width: 100%;
height: 100%;
}
.wrap-right{
float: left;
position: relative;
height: 388px;
}
.course-name{
font-size: 20px;
color: #333;
padding: 10px 23px;
letter-spacing: .45px;
}
.data{
padding-left: 23px;
padding-right: 23px;
padding-bottom: 16px;
font-size: 14px;
color: #9b9b9b;
}
.sale-time{
width: 464px;
background: #fa6240;
font-size: 14px;
color: #4a4a4a;
padding: 10px 23px;
overflow: hidden;
}
.sale-type {
font-size: 16px;
color: #fff;
letter-spacing: .36px;
float: left;
}
.sale-time .expire{
font-size: 14px;
color: #fff;
float: right;
}
.sale-time .expire .second{
width: 24px;
display: inline-block;
background: #fafafa;
color: #5e5e5e;
padding: 6px 0;
text-align: center;
}
.course-price{
background: #fff;
font-size: 14px;
color: #4a4a4a;
padding: 5px 23px;
}
.discount{
font-size: 26px;
color: #fa6240;
margin-left: 10px;
display: inline-block;
margin-bottom: -5px;
}
.original{
font-size: 14px;
color: #9b9b9b;
margin-left: 10px;
text-decoration: line-through;
}
.buy{
width: 464px;
padding: 0px 23px;
position: absolute;
left: 0;
bottom: 20px;
overflow: hidden;
}
.buy .buy-btn{
float: left;
}
.buy .buy-now{
width: 125px;
height: 40px;
border: 0;
background: #ffc210;
border-radius: 4px;
color: #fff;
cursor: pointer;
margin-right: 15px;
outline: none;
}
.buy .free{
width: 125px;
height: 40px;
border-radius: 4px;
cursor: pointer;
margin-right: 15px;
background: #fff;
color: #ffc210;
border: 1px solid #ffc210;
}
.add-cart{
float: right;
font-size: 14px;
color: #ffc210;
text-align: center;
cursor: pointer;
margin-top: 10px;
}
.add-cart img{
width: 20px;
height: 18px;
margin-right: 7px;
vertical-align: middle;
}
.course-tab{
width: 100%;
background: #fff;
margin-bottom: 30px;
box-shadow: 0 2px 4px 0 #f0f0f0;
}
.course-tab .tab-list{
width: 1200px;
margin: auto;
color: #4a4a4a;
overflow: hidden;
}
.tab-list li{
float: left;
margin-right: 15px;
padding: 26px 20px 16px;
font-size: 17px;
cursor: pointer;
}
.tab-list .active{
color: #ffc210;
border-bottom: 2px solid #ffc210;
}
.tab-list .free{
color: #fb7c55;
}
.course-content{
width: 1200px;
margin: 0 auto;
background: #FAFAFA;
overflow: hidden;
padding-bottom: 40px;
}
.course-tab-list{
width: 880px;
height: auto;
padding: 20px;
background: #fff;
float: left;
box-sizing: border-box;
overflow: hidden;
position: relative;
box-shadow: 0 2px 4px 0 #f0f0f0;
}
.tab-item{
width: 880px;
background: #fff;
padding-bottom: 20px;
box-shadow: 0 2px 4px 0 #f0f0f0;
}
.tab-item-title{
justify-content: space-between;
padding: 25px 20px 11px;
border-radius: 4px;
margin-bottom: 20px;
border-bottom: 1px solid #333;
border-bottom-color: rgba(51,51,51,.05);
overflow: hidden;
}
.chapter{
font-size: 17px;
color: #4a4a4a;
float: left;
}
.chapter-length{
float: right;
font-size: 14px;
color: #9b9b9b;
letter-spacing: .19px;
}
.chapter-title{
font-size: 16px;
color: #4a4a4a;
letter-spacing: .26px;
padding: 12px;
background: #eee;
border-radius: 2px;
display: -ms-flexbox;
display: flex;
-ms-flex-align: center;
align-items: center;
}
.chapter-title img{
width: 18px;
height: 18px;
margin-right: 7px;
vertical-align: middle;
}
.lesson-list{
padding:0 20px;
}
.lesson-list .lesson-item{
padding: 15px 20px 15px 36px;
cursor: pointer;
justify-content: space-between;
position: relative;
overflow: hidden;
}
.lesson-item .name{
font-size: 14px;
color: #666;
float: left;
}
.lesson-item .index{
margin-right: 5px;
}
.lesson-item .free{
font-size: 12px;
color: #fff;
letter-spacing: .19px;
background: #ffc210;
border-radius: 100px;
padding: 1px 9px;
margin-left: 10px;
}
.lesson-item .time{
font-size: 14px;
color: #666;
letter-spacing: .23px;
opacity: 1;
transition: all .15s ease-in-out;
float: right;
}
.lesson-item .time img{
width: 18px;
height: 18px;
margin-left: 15px;
vertical-align: text-bottom;
}
.lesson-item .try{
width: 86px;
height: 28px;
background: #ffc210;
border-radius: 4px;
font-size: 14px;
color: #fff;
position: absolute;
right: 20px;
top: 10px;
opacity: 0;
transition: all .2s ease-in-out;
cursor: pointer;
outline: none;
border: none;
}
.lesson-item:hover{
background: #fcf7ef;
box-shadow: 0 0 0 0 #f3f3f3;
}
.lesson-item:hover .name{
color: #333;
}
.lesson-item:hover .try{
opacity: 1;
}
.course-side{
width: 300px;
height: auto;
margin-left: 20px;
float: right;
}
.teacher-info{
background: #fff;
margin-bottom: 20px;
box-shadow: 0 2px 4px 0 #f0f0f0;
}
.side-title{
font-weight: normal;
font-size: 17px;
color: #4a4a4a;
padding: 18px 14px;
border-bottom: 1px solid #333;
border-bottom-color: rgba(51,51,51,.05);
}
.side-title span{
display: inline-block;
border-left: 2px solid #ffc210;
padding-left: 12px;
}
.teacher-content{
padding: 30px 20px;
box-sizing: border-box;
}
.teacher-content .cont1{
margin-bottom: 12px;
overflow: hidden;
}
.teacher-content .cont1 img{
width: 54px;
height: 54px;
margin-right: 12px;
float: left;
}
.teacher-content .cont1 .name{
float: right;
}
.teacher-content .cont1 .teacher-name{
width: 188px;
font-size: 16px;
color: #4a4a4a;
padding-bottom: 4px;
}
.teacher-content .cont1 .teacher-title{
width: 188px;
font-size: 13px;
color: #9b9b9b;
white-space: nowrap;
}
.teacher-content .narrative{
font-size: 14px;
color: #666;
line-height: 24px;
}
</style>
完善点击课程详情页的立即试学按钮跳转到视频播放页面,并发送视频的播放ID vid
在序列化器中,新增返回2个字段表示当前课时的类型和课时的视频/课件/练习题链接
class CourseLessonSerializer(serializers.ModelSerializer):
class Meta:
model = CourseLesson
fields = ["id","name","duration","free_trail","section_type","section_link"]
Detail.vue
,代码:
课时章节:
<template>
<div class="detail">
<Header/>
<div class="main">
<div class="course-info">
<div class="wrap-left">
<videoPlayer v-if="course_info.course_video" class="video-player vjs-custom-skin"
ref="videoPlayer"
:playsinline="true"
:options="playerOptions"
@play="onPlayerPlay($event)"
@pause="onPlayerPause($event)">
</videoPlayer>
<img v-else class="course_img" :src="course_info.course_img" alt="">
</div>
<div class="wrap-right">
<h3 class="course-name">{{course_info.name}}</h3>
<p class="data">{{course_info.students}}人在学 课程总时长:{{course_info.lessons}}课时/{{course_info.pub_lessons}}小时 难度:{{course_info.level_name}}</p>
<div v-if="course_info.active_time>0">
<div class="sale-time">
<p class="sale-type">{{course_info.discount_type}}</p>
<p class="expire">距离结束:仅剩{{day}}天 {{hour}}小时 {{minute}}分 <span class="second">{{second}}</span> 秒</p>
</div>
<p class="course-price">
<span>活动价</span>
<span class="discount">¥{{course_info.real_price}}</span>
<span class="original">¥{{course_info.price}}</span>
</p>
</div>
<div v-else class="sale-time">
<p class="sale-type">价格 <span class="original_price">¥{{course_info.price}}</span></p>
<p class="expire"></p>
</div>
<div class="buy">
<div class="buy-btn">
<button class="buy-now">立即购买</button>
<button class="free">免费试学</button>
</div>
<div class="add-cart" @click="add_cart(course_info.id)"><img src="/static/image/cart-yellow.svg" alt="">加入购物车</div>
</div>
</div>
</div>
<div class="course-tab">
<ul class="tab-list">
<li :class="tabIndex==1?'active':''" @click="tabIndex=1">详情介绍</li>
<li :class="tabIndex==2?'active':''" @click="tabIndex=2">课程章节 <span :class="tabIndex!=2?'free':''">(试学)</span></li>
<li :class="tabIndex==3?'active':''" @click="tabIndex=3">用户评论 (42)</li>
<li :class="tabIndex==4?'active':''" @click="tabIndex=4">常见问题</li>
</ul>
</div>
<div class="course-content">
<div class="course-tab-list">
<div class="tab-item" v-if="tabIndex==1">
<div class="course-brief" v-html="course_info.brief_text"></div>
</div>
<div class="tab-item" v-if="tabIndex==2">
<div class="tab-item-title">
<p class="chapter">课程章节</p>
<p class="chapter-length">共{{course_chapters.length}}章 {{course_info.lessons}}个课时</p>
</div>
<div class="chapter-item" v-for="chapter in course_chapters">
<p class="chapter-title"><img src="/static/image/1.svg" alt="">第{{chapter.chapter}}章·{{chapter.name}}</p>
<ul class="lesson-list">
<li class="lesson-item" v-for="lesson in chapter.coursesections">
<p class="name"><span class="index">{{chapter.chapter}}-{{lesson.id}}</span> {{lesson.name}}<span class="free" v-if="lesson.free_trail">免费</span></p>
<p class="time">{{lesson.duration}} <img src="/static/image/chapter-player.svg"></p>
<button class="try" v-if="lesson.free_trail">
<router-link v-if="lesson.section_type==0" :to="{path: '/document',query:{'vid':lesson.section_link}}">立即试学</router-link>
<router-link v-if="lesson.section_type==1" :to="{path: '/exam',query:{'vid':lesson.section_link}}">立即试学</router-link>
<router-link v-if="lesson.section_type==2" :to="{path: '/player',query:{'vid':lesson.section_link}}">立即试学</router-link>
</button>
<button class="try" v-else>立即购买</button>
</li>
</ul>
</div>
</div>
<div class="tab-item" v-if="tabIndex==3">
用户评论
</div>
<div class="tab-item" v-if="tabIndex==4">
常见问题
</div>
</div>
<div class="course-side">
<div class="teacher-info">
<h4 class="side-title"><span>授课老师</span></h4>
<div class="teacher-content">
<div class="cont1">
<img :src="course_info.teacher.image">
<div class="name">
<p class="teacher-name">{{course_info.teacher.name}} {{course_info.teacher.title}}</p>
<p class="teacher-title">{{course_info.teacher.signature}}</p>
</div>
</div>
<p class="narrative" >{{course_info.teacher.brief}}</p>
</div>
</div>
</div>
</div>
</div>
<Footer/>
</div>
</template>
<script>
import Header from "./common/Header"
import Footer from "./common/Footer"
// 加载组件
import {videoPlayer} from 'vue-video-player';
export default {
name: "Detail",
data(){
return {
tabIndex:2, // 当前选项卡显示的下标
course_id: 0, // 当前课程信息的ID
course_info: {
teacher:{},
}, // 课程信息
course_chapters:[], // 课程的章节课时列表
playerOptions: {
playbackRates: [0.7, 1.0, 1.5, 2.0], // 播放速度
autoplay: false, //如果true,则自动播放
muted: false, // 默认情况下将会消除任何音频。
loop: false, // 循环播放
preload: 'auto', // 建议浏览器在<video>加载元素后是否应该开始下载视频数据。auto浏览器选择最佳行为,立即开始加载视频(如果浏览器支持)
language: 'zh-CN',
aspectRatio: '16:9', // 将播放器置于流畅模式,并在计算播放器的动态大小时使用该值。值应该代表一个比例 - 用冒号分隔的两个数字(例如"16:9"或"4:3")
fluid: true, // 当true时,Video.js player将拥有流体大小。换句话说,它将按比例缩放以适应其容器。
sources: [{ // 播放资源和资源格式
type: "video/mp4",
src: "" //你的视频地址(必填)
}],
poster: "", //视频封面图
width: document.documentElement.clientWidth, // 默认视频全屏时的最大宽度
notSupportedMessage: '此视频暂无法播放,请稍后再试', //允许覆盖Video.js无法播放媒体源时显示的默认信息。
}
}
},
computed:{
day(){
let day = parseInt( this.course_info.active_time/ (24*3600));
if(day < 10){
return '0'+day;
}else{
return day;
}
},
hour(){
let rest = parseInt( this.course_info.active_time % (24*3600) );
let hours = parseInt(rest/3600);
if(hours < 10){
return '0'+hours;
}else{
return hours;
}
},
minute(){
let rest = parseInt( this.course_info.active_time % 3600 );
let minute = parseInt(rest/60);
if(minute < 10){
return '0'+minute;
}else{
return minute;
}
},
second(){
let second = this.course_info.active_time % 60;
if(second < 10){
return '0'+second;
}else{
return second;
}
}
},
created(){
this.get_course_id();
this.get_course_data();
this.get_chapter();
},
methods: {
onPlayerPlay(event){
// 当视频播放时,执行的方法
alert("关闭广告")
},
onPlayerPause(event){
// 当视频暂停播放时,执行的方法
alert("显示广告");
},
get_course_id(){
// 获取地址栏上面的课程ID
this.course_id = this.$route.params.course;
if( this.course_id < 1 ){
let _this = this;
_this.$alert("对不起,当前视频不存在!","警告",{
callback(){
_this.$router.go(-1);
}
});
}
},
get_course_data(){
// ajax请求课程信息
this.$axios.get(`${this.$settings.Host}/courses/${this.course_id}/`).then(response=>{
// console.log(response.data);
this.course_info = response.data;
this.playerOptions.poster = response.data.course_img;
// 在服务端中新增一个模型字段 course_video,如果有视频,则显示到播放器中,如果没有则显示一张封面图片
if(response.data.course_video){
// this.playerOptions.sources[0].src = "http://img.ksbbs.com/asset/Mon_1703/05cacb4e02f9d9e.mp4";
this.playerOptions.sources[0].src = response.data.course_video;
}
// 在获取到剩余的活动时间以后,就要进入倒计时
let timer = setInterval(()=>{
if(this.course_info.active_time<1){
clearInterval(timer);
}else{
--this.course_info.active_time;
}
},1000);
}).catch(response=>{
this.$message({
message:"对不起,访问页面出错!请联系客服工作人员!"
});
})
},
get_chapter(){
// 获取当前课程对应的章节课时信息
this.$axios.get(`${this.$settings.Host}/courses/chapters/`,{
params:{
"course": this.course_id,
}
}).then(response=>{
this.course_chapters = response.data;
}).catch(error=>{
console.log(error.response);
})
},
add_cart(course_id){
// 添加商品到购物车
// 验证用户登录状态,如果登录了则可以添加商品到购物车,如果没有登录则跳转到登录界面,登录完成以后,才能添加商品到购物车
let token = localStorage.token || sessionStorage.token;
if( !token ){
this.$confirm("对不起,您尚未登录,请登录以后再进行购物车").then(()=>{
this.$router.push("/login/");
});
return false; // 阻止代码往下执行
}
// 添加商品到购物车,因为购物车接口必须用户是登录的,所以我们要在请求头中设置 jwttoken
this.$axios.post(`${this.$settings.Host}/cart/`,{
"course_id": course_id,
},{
headers:{
"Authorization":"jwt " + token,
}
}).then(response=>{
this.$message({
message:response.data.message,
});
// 购物车中的商品数量
let total = response.data.total;
this.$store.commit("change_total",total)
}).catch(error=>{
this.$message({
message:error.response.data
})
})
}
},
components:{
Header,
Footer,
videoPlayer, // 注册组件
}
}
</script>
Player.vue
,代码:
获取vid视频ID
<template>
<div class="player">
<div id="player"></div>
</div>
</template>
<script>
export default {
name:"Player",
data () {
return {
}
},
methods: {
check_login(){
// 检查当前访问者是否登录了!
let token = localStorage.token || sessionStorage.token;
if( !token ){
this.$alert("对不起,您尚未登录,请登录以后再进行购物车").then(()=>{
this.$router.push("/login/");
});
return false; // 阻止代码往下执行
}
return token;
},
},
mounted(){
// 验证用户是否登录
let token = this.check_login();
let user_name = localStorage.user_name || sessionStorage.user_name;
let _this = this;
let vid = this.$route.query.vid;
var player = polyvObject('#player').videoPlayer({
wrap: '#player',
width: document.documentElement.clientWidth-260, // 页面宽度
height: document.documentElement.clientHeight, // 页面高度
forceH5: true,
vid: vid,
code: user_name, // 一般是用户昵称
// 视频加密播放的配置
playsafe: function (vid, next) { // 向后端发送请求获取加密的token
_this.$axios.get(`${_this.$settings.Host}/courses/polyv/token/`,{
params:{
vid: vid,
},
headers:{
"Authorization":"jwt " + token,
}
}).then(function (response) {
console.log(response);
next(response.data.token);
})
}
});
},
computed: {
}
}
</script>
<style scoped>
</style>
完善API接口的身份认证
试学必须在用户登录以后才能进行,所以后端的tokenAPI接口必须保证用户登陆以后,我们已经完成了。
但是有些视频必须购买了以后才能播放!对于这种情况,在用户点击播放的时候,我们在后端返回token时,进行数据库查询,这个用户是否购买了课程,并且课程在有效期范围内!