路飛專案主體
阿新 • • 發佈:2020-08-03
目錄
路飛專案
1 路飛專案頭部元件
1.1 路由跳轉的方式
#html中路由跳轉
<router-link to="/">
<img src="../assets/img/head-logo.svg" alt="">
</router-link>
#js中控制路由跳轉
this.$router.push('/');
1.2 頭部元件vue程式碼
# 在components內新建Head.vue <template> <div class="header"> <div class="slogan"> <p>老男孩IT教育 | 幫助有志向的年輕人通過努力學習獲得體面的工作和生活</p> </div> <div class="nav"> <ul class="left-part"> <li class="logo"> <router-link to="/"> <img src="../assets/img/head-logo.svg" alt=""> </router-link> </li> <li class="ele"> <span @click="goPage('/free-course')" :class="{active: url_path === '/free-course'}">免費課</span> </li> <li class="ele"> <span @click="goPage('/actual-course')" :class="{active: url_path === '/actual-course'}">實戰課</span> </li> <li class="ele"> <span @click="goPage('/light-course')" :class="{active: url_path === '/light-course'}">輕課</span> </li> </ul> <div class="right-part"> <div> <span>登入</span> <span class="line">|</span> <span>註冊</span> </div> </div> </div> </div> </template> <script> export default { name: "Header", data() { return { url_path: sessionStorage.url_path || '/', } }, methods: { goPage(url_path) { // 已經是當前路由就沒有必要重新跳轉 if (this.url_path !== url_path) { this.$router.push(url_path); } sessionStorage.url_path = url_path; }, }, created() { sessionStorage.url_path = this.$route.path; this.url_path = this.$route.path; } } </script> <style scoped> .header { background-color: white; box-shadow: 0 0 5px 0 #aaa; } .header:after { content: ""; display: block; clear: both; } .slogan { background-color: #eee; height: 40px; } .slogan p { width: 1200px; margin: 0 auto; color: #aaa; font-size: 13px; line-height: 40px; } .nav { background-color: white; user-select: none; width: 1200px; margin: 0 auto; } .nav ul { padding: 15px 0; float: left; } .nav ul:after { clear: both; content: ''; display: block; } .nav ul li { float: left; } .logo { margin-right: 20px; } .ele { margin: 0 20px; } .ele span { display: block; font: 15px/36px '微軟雅黑'; border-bottom: 2px solid transparent; cursor: pointer; } .ele span:hover { border-bottom-color: orange; } .ele span.active { color: orange; border-bottom-color: orange; } .right-part { float: right; } .right-part .line { margin: 0 10px; } .right-part span { line-height: 68px; cursor: pointer; } </style>
2 路飛專案尾部元件
Footer.vue
<template>
<div class="footer">
<ul>
<li>關於我們</li>
<li>聯絡我們</li>
<li>商務合作</li>
<li>幫助中心</li>
<li>意見反饋</li>
<li>新手指南</li>
</ul>
<p>Copyright © luffycity.com版權所有 | 京ICP備17072161號-1</p>
</div>
</template>
<script>
export default {
name: "Footer"
}
</script>
<style scoped>
.footer {
width: 100%;
height: 128px;
background: #25292e;
color: #fff;
}
.footer ul {
margin: 0 auto 16px;
padding-top: 38px;
width: 810px;
}
.footer ul li {
float: left;
width: 112px;
margin: 0 10px;
text-align: center;
font-size: 14px;
}
.footer ul::after {
content: "";
display: block;
clear: both;
}
.footer p {
text-align: center;
font-size: 12px;
}
</style>
3 輪播圖
3.1 輪播圖介面
# 1 urls.py
from rest_framework.routers import SimpleRouter
router=SimpleRouter()
router.register('banner',views.BannerView,'banner')
urlpatterns = [
# path('banner/', views.BannerView.as_view()),
path('', include(router.urls)),
]
# 2 views.py
from django.conf import settings
class BannerView(GenericViewSet,ListModelMixin):
# 無論有多少條待展示的資料,最多就展示3條
queryset = models.Banner.objects.filter(is_delete=False,is_show=True).order_by('display_order')[:settings.BANNER_COUNTER]
serializer_class = serializer.BannerModelSerilaizer
# 3 serializer.py
from rest_framework import serializers
from . import models
class BannerModelSerilaizer(serializers.ModelSerializer):
class Meta:
model=models.Banner
fields=['name','link','img']
# 4 models.py
from utils.models import BaseModel
class Banner(BaseModel):
name=models.CharField(max_length=32,verbose_name='圖片名字')
img=models.ImageField(upload_to='banner',verbose_name='輪播圖',help_text='圖片尺寸必須是:3840*800',null=True)
link=models.CharField(max_length=32,verbose_name='跳轉連線')
info=models.TextField(verbose_name='圖片簡介')
# type=models.IntegerField(choices=)
def __str__(self):
return self.name
# 5 utils/models.py
class BaseModel(models.Model):
create_time = models.DateTimeField(auto_now_add=True, verbose_name='建立時間')
update_time = models.DateTimeField(auto_now=True, verbose_name='最後更新時間')
is_delete = models.BooleanField(default=False, verbose_name='是否刪除')
is_show = models.BooleanField(default=True, verbose_name='是否展示')
display_order = models.IntegerField()
class Meta:
abstract=True # 一定不要忘了
# 6 settings/const.py
# 首頁輪播圖個數
BANNER_COUNTER=3
# 7 settings/dev.py
from .const import *
3.2 輪播圖元件
Banner.vue
<template>
<div id="banner">
<el-carousel height="400px">
<el-carousel-item v-for="item in banner_list">
<!--<img src="../assets/img/banner1.png" alt="">-->
<router-link :to="item.link">
<img :src="item.img" :alt="item.name">
</router-link>
</el-carousel-item>
</el-carousel>
</div>
</template>
<script>
export default {
name: "Banner",
// data:function(){},
data() {
return {
banner_list: []
}
},
created() {
//當banner元件一建立,就向後臺發請求,拿回輪播圖資料
this.$axios.get(this.$settings.base_url + '/home/banner/').then(response => {
console.log(response.data)
this.banner_list=response.data
}).catch(error => {
})
},
}
</script>
<style scoped>
.el-carousel__item {
height: 400px;
min-width: 1200px;
}
.el-carousel__item img {
height: 400px;
/*margin-left: 20px;*/
/*margin-left: calc(50% - 1920px / 2);*/
}
</style>
前端路由配置(index.js)
const routes = [
{
path: '/',
name: 'Home',
component: Home
},
{
path: '/free-course',
name: 'FreeCourse',
component: FreeCourse
}, {
path: '/light-course',
name: 'LightCourse',
component: LightCourse
}, {
path: '/actual-course',
name: 'ActualCourse',
component: ActualCourse
},
]
3.3 輪播圖快取
使用django-redis模組
pip install django-redis
在django專案的setting中配置
CACHES = {
"default": {
"BACKEND": "django_redis.cache.RedisCache",
"LOCATION": "redis://127.0.0.1:6379",
"OPTIONS": {
"CLIENT_CLASS": "django_redis.client.DefaultClient",
"CONNECTION_POOL_KWARGS": {"max_connections": 100}
# "PASSWORD": "123",
}
}
}
# 首頁輪播圖資料快取到redis中
# 重寫輪播圖介面的list方法
def list(self, request, *args, **kwargs):
# response=super().list(request, *args, **kwargs)
# 把data的資料加快取,先去快取拿資料
banner_list=cache.get('banner_list')
if not banner_list:
print('走資料庫了')
# 快取中沒有,去資料庫拿
response = super().list(request, *args, **kwargs)
# 加到快取
cache.set('banner_list',response.data,60*60*24)
return response
return Response(data=banner_list)
3.4 輪播圖非同步更新
pip install celery
# celery.py
from celery import Celery
# 載入django環境
import os
import django
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "luffyapi.settings.dev")
django.setup()
broker='redis://127.0.0.1:6379/1' #broker任務佇列
backend='redis://127.0.0.1:6379/2' # 結構儲存,執行完的結果存在這
app=Celery(__name__,broker=broker,backend=backend,include=['celery_task.home_task',])
# 執行定時任務
# 時區
app.conf.timezone = 'Asia/Shanghai'
# 是否使用UTC
app.conf.enable_utc = False
# 任務的定時配置
from datetime import timedelta
from celery.schedules import crontab
app.conf.beat_schedule = {
'add-task': {
'task': 'celery_task.home_task.banner_update',
'schedule': timedelta(seconds=30),
}
}
# home_task.py
from .celery import app
@app.task
def banner_update():
from home import serializer
from home import models
from django.conf import settings
from django.core.cache import cache
queryset_banner = models.Banner.objects.filter(is_delete=False, is_show=True).order_by('display_order')[
:settings.BANNER_COUNTER]
serializer_banner=serializer.BannerModelSerilaizer(instance=queryset_banner,many=True)
# print(serializer_banner.data)
for banner in serializer_banner.data:
banner['img']='http://127.0.0.1:8000'+banner['img']
cache.set('banner_list',serializer_banner.data)
# import time
# time.sleep(1)
# banner_list=cache.get('banner_list')
# print(banner_list)
return True
4 登入
4.1 vue登入元件雛形
# Login.vue
<template>
<div class="login">
<span @click="close_login">X</span>
</div>
</template>
<script>
export default {
name: "Login",
methods:{
close_login(){
//子傳父元件 this.$emit,給父元件傳遞一個事件
this.$emit("close")
},
},
}
</script>
<style scoped>
.login{
width: 100vw;
height: 100vw;
position: fixed;
left: 0;
top: 0;
z-index: 666;
background-color: rgba(0,0,0,0.3);
}
span{
font-size: 30px;
cursor: pointer;
}
</style>
# Head.vue
#template中
<div class="right-part">
<div>
<span @click="pull_login">登入</span>
<span class="line">|</span>
<span>註冊</span>
</div>
</div>
</div>
<Login v-if="is_login" @close="close"/>
<!--is_login是True就顯示,false就不顯示-->
</div>
# js中
pull_login(){
this.is_login=true
},
close(){
this.is_login=false
},
4.2 後端登入介面雛形
from rest_framework.viewsets import GenericViewSet
from luffyapi.utils.response import APIResponse
from rest_framework.decorators import action
class LoginView(GenericViewSet):
queryset = models.User.objects.all()
serializer_class = ser.LoginSerializer
authentication_classes = []
permission_classes = []
@action(methods=['post'], detail=False)
def login(self, request):
serializer = self.get_serializer(data=request.data)
if not serializer.is_valid():
return APIResponse(code=0, msg=serializer.errors)
token = serializer.context.get('token')
username = serializer.context.get('username')
return APIResponse(token=token, username=username, code=1, msg='登陸成功', status=200)
序列化器(多方式登入)
import re
from rest_framework import serializers
from rest_framework.exceptions import ValidationError
from rest_framework_jwt.utils import jwt_encode_handler, jwt_decode_handler, jwt_payload_handler
from user import models
class LoginSerializer(serializers.ModelSerializer):
username = serializers.CharField()
class Meta:
model = models.User
fields = ['username', 'password']
def validate(self, attrs):
username = attrs.get('username')
password = attrs.get('password')
if re.match('^1[3-9][0-9]{9}$', username):
user = models.User.objects.filter(telephone=username)
elif re.match('^.+@.+$', username):
user = models.User.objects.filter(email=username)
else:
user = models.User.objects.filter(username=username)
if user:
user = user.first()
if user.check_password(password):
payload = jwt_payload_handler(user)
token = jwt_encode_handler(payload)
self.context['token'] = token
self.context['username'] = user.username
return attrs
else:
raise ValidationError('密碼錯誤')
else:
raise ValidationError('使用者名稱不存在')
4.3 vue登入元件第二版
Login.vue
<template>
<div class="login">
<div class="box">
<i class="el-icon-close" @click="close_login"></i>
<div class="content">
<div class="nav">
<span :class="{active: login_method === 'is_pwd'}"
@click="change_login_method('is_pwd')">密碼登入</span>
<span :class="{active: login_method === 'is_sms'}"
@click="change_login_method('is_sms')">簡訊登入</span>
</div>
<el-form v-if="login_method === 'is_pwd'">
<el-input
placeholder="使用者名稱/手機號/郵箱"
prefix-icon="el-icon-user"
v-model="username"
clearable>
</el-input>
<el-input
placeholder="密碼"
prefix-icon="el-icon-key"
v-model="password"
clearable
show-password>
</el-input>
<el-button type="primary" @click="login">登入</el-button>
</el-form>
<el-form v-if="login_method === 'is_sms'">
<el-input
placeholder="手機號"
prefix-icon="el-icon-phone-outline"
v-model="mobile"
clearable
@blur="check_mobile">
</el-input>
<el-input
placeholder="驗證碼"
prefix-icon="el-icon-chat-line-round"
v-model="sms"
clearable>
<template slot="append">
<span class="sms" @click="send_sms">{{ sms_interval }}</span>
</template>
</el-input>
<el-button type="primary">登入</el-button>
</el-form>
<div class="foot">
<span @click="go_register">立即註冊</span>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: "Login",
data() {
return {
username: '',
password: '',
mobile: '',
sms: '',
login_method: 'is_pwd',
sms_interval: '獲取驗證碼',
is_send: false,
}
},
methods: {
close_login() {
this.$emit('close')
},
go_register() {
this.$emit('go')
},
change_login_method(method) {
this.login_method = method;
},
check_mobile() {
if (!this.mobile) return;
if (!this.mobile.match(/^1[3-9][0-9]{9}$/)) {
this.$message({
message: '手機號有誤',
type: 'warning',
duration: 1000,
onClose: () => {
this.mobile = '';
}
});
return false;
}
this.is_send = true;
},
send_sms() {
if (!this.is_send) return;
this.is_send = false;
let sms_interval_time = 60;
this.sms_interval = "傳送中...";
let timer = setInterval(() => {
if (sms_interval_time <= 1) {
clearInterval(timer);
this.sms_interval = "獲取驗證碼";
this.is_send = true; // 重新回覆點擊發送功能的條件
} else {
sms_interval_time -= 1;
this.sms_interval = `${sms_interval_time}秒後再發`;
}
}, 1000);
},
login(){
if(this.username&&this.password){
this.$axios.post(this.$settings.base_url+'/user/login/',{username:this.username,password:this.password})
.then(response=>{
if(response.data.code===0){this.$message.error(response.data.msg.non_field_errors[0])}
else if(response.data.code===1){
this.$cookies.set('token',response.data.token,'7d')
this.$cookies.set('username',response.data.username,'7d')
this.username=''
this.password=''
this.$emit('update_head')
this.close_login()
}
}
).catch(errors=>{})
}else {
this.$message.error('使用者名稱和密碼不能為空');
}
}
}
}
</script>
<style scoped>
.login {
width: 100vw;
height: 100vh;
position: fixed;
top: 0;
left: 0;
z-index: 10;
background-color: rgba(0, 0, 0, 0.3);
}
.box {
width: 400px;
height: 420px;
background-color: white;
border-radius: 10px;
position: relative;
top: calc(50vh - 210px);
left: calc(50vw - 200px);
}
.el-icon-close {
position: absolute;
font-weight: bold;
font-size: 20px;
top: 10px;
right: 10px;
cursor: pointer;
}
.el-icon-close:hover {
color: darkred;
}
.content {
position: absolute;
top: 40px;
width: 280px;
left: 60px;
}
.nav {
font-size: 20px;
height: 38px;
border-bottom: 2px solid darkgrey;
}
.nav > span {
margin: 0 20px 0 35px;
color: darkgrey;
user-select: none;
cursor: pointer;
padding-bottom: 10px;
border-bottom: 2px solid darkgrey;
}
.nav > span.active {
color: black;
border-bottom: 3px solid black;
padding-bottom: 9px;
}
.el-input, .el-button {
margin-top: 40px;
}
.el-button {
width: 100%;
font-size: 18px;
}
.foot > span {
float: right;
margin-top: 20px;
color: orange;
cursor: pointer;
}
.sms {
color: orange;
cursor: pointer;
display: inline-block;
width: 70px;
text-align: center;
user-select: none;
}
</style>
Head.vue
<template>
<div class="header">
<div class="slogan">
<p>老男孩IT教育 | 幫助有志向的年輕人通過努力學習獲得體面的工作和生活</p>
</div>
<div class="nav">
<ul class="left-part">
<li class="logo">
<router-link to="/">
<img src="../assets/img/head-logo.svg" alt="">
</router-link>
</li>
<li class="ele">
<span @click="goPage('/free-course')" :class="{active: url_path === '/free-course'}">免費課</span>
</li>
<li class="ele">
<span @click="goPage('/actual-course')" :class="{active: url_path === '/actual-course'}">實戰課</span>
</li>
<li class="ele">
<span @click="goPage('/light-course')" :class="{active: url_path === '/light-course'}">輕課</span>
</li>
</ul>
<div class="right-part">
<div v-show="!username">
<span @click="login_show=!login_show">登入</span>
<span class="line">|</span>
<span @click="reg_show=!reg_show">註冊</span>
</div>
<div v-show="username">
<span>{{username}}</span>
<span class="line">|</span>
<span @click="logout">登出</span>
</div>
</div>
<Login @close="login_show=!login_show" @go="goReg" @update_head='update_head' v-show="login_show"></Login>
<Reg @close="reg_show=!reg_show" @go="goLogin" v-show="reg_show"></Reg>
</div>
</div>
</template>
<script>
import Login from "./Login";
import Reg from "./Reg";
export default {
name: "Header",
data() {
return {
url_path: sessionStorage.url_path || '/',
login_show:false,
reg_show:false,
token:'',
username:''
}
},
methods: {
goPage(url_path) {
// 已經是當前路由就沒有必要重新跳轉
if (this.url_path !== url_path) {
this.$router.push(url_path);
}
sessionStorage.url_path = url_path;
},
goReg(){
this.login_show=false
this.reg_show=true
},
goLogin(){
this.reg_show=false
this.login_show=true
},
logout(){
this.$cookies.remove('username')
this.$cookies.remove('token')
this.username=''
this.token=''
},
update_head(){
this.token = this.$cookies.get('token');
this.username = this.$cookies.get('username');
}
},
created() {
sessionStorage.url_path = this.$route.path;
this.url_path = this.$route.path;
this.token = this.$cookies.get('token');
this.username = this.$cookies.get('username');
},
components:{
Login,
Reg
}
}
</script>
<style scoped>
.header {
background-color: white;
box-shadow: 0 0 5px 0 #aaa;
}
.header:after {
content: "";
display: block;
clear: both;
}
.slogan {
background-color: #eee;
height: 40px;
}
.slogan p {
width: 1200px;
margin: 0 auto;
color: #aaa;
font-size: 13px;
line-height: 40px;
}
.nav {
background-color: white;
user-select: none;
width: 1200px;
margin: 0 auto;
}
.nav ul {
padding: 15px 0;
float: left;
}
.nav ul:after {
clear: both;
content: '';
display: block;
}
.nav ul li {
float: left;
}
.logo {
margin-right: 20px;
}
.ele {
margin: 0 20px;
}
.ele span {
display: block;
font: 15px/36px '微軟雅黑';
border-bottom: 2px solid transparent;
cursor: pointer;
}
.ele span:hover {
border-bottom-color: orange;
}
.ele span.active {
color: orange;
border-bottom-color: orange;
}
.right-part {
float: right;
}
.right-part .line {
margin: 0 10px;
}
.right-part span {
line-height: 68px;
cursor: pointer;
}
</style>
4.4 後端登入介面第二版
from rest_framework.viewsets import GenericViewSet
from django.core.cache import cache
from user import models
from user import ser
import re
from luffyapi.utils.response import APIResponse
from rest_framework.decorators import action
from luffyapi.lib.tx_sms import send_message, get_code, expiry, PHONE_CACHE_KEY
# Create your views here.
class LoginView(GenericViewSet):
queryset = models.User.objects.all()
serializer_class = ser.LoginSerializer
authentication_classes = []
permission_classes = []
@action(methods=['post'], detail=False)
def login(self, request):
serializer = self.get_serializer(data=request.data)
if not serializer.is_valid():
return APIResponse(code=0, msg=serializer.errors)
token = serializer.context.get('token')
username = serializer.context.get('username')
return APIResponse(token=token, username=username, code=1, msg='登陸成功', status=200)
@action(methods=['post'], detail=False)
def check_phone(self, request):
telephone = request.data.get('telephone')
if not re.match('^1[3-9][0-9]{9}$', telephone):
return APIResponse(code=999, msg='手機號不合法')
user = models.User.objects.filter(telephone=telephone)
if user:
return APIResponse(code=1, msg='手機號存在')
else:
return APIResponse(code=0, msg='手機號未註冊')
@action(methods=['post'], detail=False)
def send_sms(self, request):
telephone = request.data.get('telephone')
if not re.match('^1[3-9][0-9]{9}$', telephone):
return APIResponse(code=999, msg='手機號不合法')
user = models.User.objects.filter(telephone=telephone)
if user:
code = get_code()
cache.set(PHONE_CACHE_KEY % telephone, code, expiry)
print(code)
result = send_message(telephone, code)
if result:
return APIResponse(code=1, msg='傳送成功')
else:
return APIResponse(code=999, msg='未知錯誤')
else:
return APIResponse(code=0, msg='手機號未註冊')
4.5 傳送簡訊功能
pip install qcloudsms_py
send.py
from luffyapi.utils.logger import log
from .settings import *
import random
from qcloudsms_py import SmsSingleSender
# 生成一個四位隨機驗證碼
def get_code():
s_code = ''
for i in range(4):
s_code += str(random.randint(0, 9))
return s_code
def send_message(phone, code):
ssender = SmsSingleSender(appid, appkey)
params = [code, expiry // 60]
try:
result = ssender.send_with_param(86, phone, template_id, params, sign=sms_sign, extend="", ext="")
if result.get('result') == 0:
return True
else:
return False
except Exception as e:
log.error(f'傳送簡訊給{phone}失敗,錯誤為{e}')
return False
settings.py
# 簡訊應用 SDK AppID
appid = 1400403285 # SDK AppID 以1400開頭
# 簡訊應用 SDK AppKey
appkey = "139d9d5db909d2f969fd3c533976482c"
# 簡訊過期時間(秒)
expiry = 300
# 簡訊模板ID,需要在簡訊控制檯中申請
template_id = 669369 # NOTE: 這裡的模板 ID`7839`只是示例,真實的模板 ID 需要在簡訊控制檯中申請
# 簽名
sms_sign = "從零開始的py程式猿生活" # NOTE: 簽名引數使用的是`簽名內容`,而不是`簽名ID`。這裡的簽名"騰訊雲
#
PHONE_CACHE_KEY = 'sms_cache_%s'
4.6 簡訊介面頻率限制
# throttlings.py
from rest_framework.throttling import SimpleRateThrottle
class SMSThrottle(SimpleRateThrottle):
scope = 'sms'
def get_cache_key(self, request, view):
return self.cache_format % {'scope': self.scope, 'ident': request.data.get('telephone')}
# 將頻率限制配置在檢視類上
from .throttlings import SMSThrotting
class SendSmSView(ViewSet):
throttle_classes = [SMSThrotting,]
# 可以用區域性的區域性:裝飾器
@action(methods=['post'], detail=False, throttle_classes=[SMSThrottle])
def login_send_sms(self, request):
# 在setting中配置頻率
REST_FRAMEWORK = {
'DEFAULT_THROTTLE_RATES':{
'sms':'1/m' # key要跟類中的scop對應
}
}
4.7 驗證碼登陸介面
# views.py
@action(methods=['POST'],detail=False)
def code_login(self,request,*args,**kwargs):
ser = serializer.CodeUserSerilaizer(data=request.data)
if ser.is_valid():
token = ser.context['token']
username = ser.context['user'].username
return APIResponse(token=token, username=username)
else:
return APIResponse(code=0,msg=ser.errors)
# serializer
class CodeUserSerilaizer(serializers.ModelSerializer):
code=serializers.CharField()
class Meta:
model = models.User
fields = ['telephone', 'code']
def validate(self, attrs):
user=self._get_user(attrs)
# 使用者存在,簽發token
token = self._get_token(user)
self.context['token'] = token
self.context['user'] = user
return attrs
def _get_user(self, attrs):
from django.core.cache import cache
from django.conf import settings
import re
telephone = attrs.get('telephone')
code = attrs.get('code')
# 取出原來的code
cache_code=cache.get(settings.PHONE_CACHE_KEY%telephone)
if code ==cache_code:
# 驗證碼通過
if re.match('^1[3-9][0-9]{9}$', telephone):
user = models.User.objects.filter(telephone=telephone).first()
if user:
# 把使用過的驗證碼刪除
cache.set(settings.PHONE_CACHE_KEY % telephone,'')
return user
else:
raise ValidationError('使用者不存在')
else:
raise ValidationError('手機號不合法')
else:
raise ValidationError('驗證碼錯誤')
def _get_token(self, user):
from rest_framework_jwt.serializers import jwt_payload_handler, jwt_encode_handler
payload = jwt_payload_handler(user) # 通過user物件獲得payload
token = jwt_encode_handler(payload) # 通過payload獲得token
return token
4.8 前臺傳送驗證碼
<template>
<div class="login">
<div class="box">
<i class="el-icon-close" @click="close_login"></i>
<div class="content">
<div class="nav">
<span :class="{active: login_method === 'is_pwd'}"
@click="change_login_method('is_pwd')">密碼登入</span>
<span :class="{active: login_method === 'is_sms'}"
@click="change_login_method('is_sms')">簡訊登入</span>
</div>
<el-form v-if="login_method === 'is_pwd'">
<el-input
placeholder="使用者名稱/手機號/郵箱"
prefix-icon="el-icon-user"
v-model="username"
clearable>
</el-input>
<el-input
placeholder="密碼"
prefix-icon="el-icon-key"
v-model="password"
clearable
show-password>
</el-input>
<el-button type="primary" @click="login_password">登入</el-button>
</el-form>
<el-form v-if="login_method === 'is_sms'">
<el-input
placeholder="手機號"
prefix-icon="el-icon-phone-outline"
v-model="mobile"
clearable
@blur="check_mobile">
</el-input>
<el-input
placeholder="驗證碼"
prefix-icon="el-icon-chat-line-round"
v-model="sms"
clearable>
<template slot="append">
<span class="sms" @click="send_sms">{{ sms_interval }}</span>
</template>
</el-input>
<el-button type="primary" @click="code_login">登入</el-button>
</el-form>
<div class="foot">
<span @click="go_register">立即註冊</span>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: "Login",
data() {
return {
username: '',
password: '',
mobile: '',
sms: '',
login_method: 'is_pwd',
sms_interval: '獲取驗證碼',
is_send: false,
}
},
methods: {
close_login() {
this.$emit('close')
},
go_register() {
this.$emit('go')
},
change_login_method(method) {
this.login_method = method;
},
check_mobile() {
if (!this.mobile) return;
//字串.match(/正則表示式/)
if (!this.mobile.match(/^1[3-9][0-9]{9}$/)) {
this.$message({
message: '手機號有誤',
type: 'warning',
duration: 1000,
onClose: () => {
this.mobile = '';
}
});
return false;
}
//傳送axios請求
// this.$axios.get(this.$settings.base_url+'/user/check_telephone/telephone='+this.mobile})
this.$axios.get(this.$settings.base_url + '/user/check_telephone/', {params: {telephone: this.mobile}}).then(response => {
if (response.data.code) {
//手機號存在,允許傳送驗證碼
this.is_send = true;
} else {
this.$message({
message: '手機號不存在',
type: 'warning',
duration: 1000,
onClose: () => {
this.mobile = '';
}
});
}
}).catch(error => {
console.log(error)
})
},
send_sms() {
if (!this.is_send) return;
this.is_send = false;
let sms_interval_time = 60;
this.sms_interval = "傳送中...";
this.$axios.get(this.$settings.base_url + '/user/send/', {params: {'telephone': this.mobile}})
.then(response => {
if (response.data.code) {
this.$message({
message: '傳送驗證碼成功',
type: 'success',
duration: 1000,
});
}
})
// setInterval(()=>{},100)
//定時器:每隔一秒種,把數字減一,當減到小於1,按鈕又能點了,顯示獲取驗證碼
let timer = setInterval(() => {
if (sms_interval_time <= 1) {
clearInterval(timer); //如果小於等於1,把定時器清除
this.sms_interval = "獲取驗證碼";
this.is_send = true; // 重新回覆點擊發送功能的條件
} else {
sms_interval_time -= 1;
this.sms_interval = `${sms_interval_time}秒後再發`;
}
}, 1000);
},
login_password() {
if (this.username && this.password) {
//傳送請求
this.$axios.post(this.$settings.base_url + '/user/login/', {
username: this.username,
password: this.password
}).then(response => {
console.log(response.data)
//把使用者資訊儲存到cookie中
// this.$cookies.set('key','value','過期時間,按s計')
this.$cookies.set('token', response.data.token, '7d')
this.$cookies.set('username', response.data.username, '7d')
//關閉登入視窗(子傳父)
this.$emit('close')
//給父元件,Head傳遞一個事件,讓它從cookie中取出token和username
this.$emit('loginsuccess')
}).catch(errors => {
})
} else {
this.$message({
message: '使用者名稱或密碼必須填哦',
type: 'warning',
});
}
},
code_login() {
if (this.mobile && this.sms) {
//傳送請求
this.$axios.post(this.$settings.base_url + '/user/code_login/', {
telephone: this.mobile,
code: this.sms
}).then(response => {
console.log(response.data)
//把使用者資訊儲存到cookie中
// this.$cookies.set('key','value','過期時間,按s計')
this.$cookies.set('token', response.data.token, '7d')
this.$cookies.set('username', response.data.username, '7d')
//關閉登入視窗(子傳父)
this.$emit('close')
//給父元件,Head傳遞一個事件,讓它從cookie中取出token和username
this.$emit('loginsuccess')
}).catch(errors => {
})
} else {
this.$message({
message: '手機號或驗證碼必填',
type: 'warning',
});
}
},
}
}
</script>
<style scoped>
.login {
width: 100vw;
height: 100vh;
position: fixed;
top: 0;
left: 0;
z-index: 10;
background-color: rgba(0, 0, 0, 0.3);
}
.box {
width: 400px;
height: 420px;
background-color: white;
border-radius: 10px;
position: relative;
top: calc(50vh - 210px);
left: calc(50vw - 200px);
}
.el-icon-close {
position: absolute;
font-weight: bold;
font-size: 20px;
top: 10px;
right: 10px;
cursor: pointer;
}
.el-icon-close:hover {
color: darkred;
}
.content {
position: absolute;
top: 40px;
width: 280px;
left: 60px;
}
.nav {
font-size: 20px;
height: 38px;
border-bottom: 2px solid darkgrey;
}
.nav > span {
margin: 0 20px 0 35px;
color: darkgrey;
user-select: none;
cursor: pointer;
padding-bottom: 10px;
border-bottom: 2px solid darkgrey;
}
.nav > span.active {
color: black;
border-bottom: 3px solid black;
padding-bottom: 9px;
}
.el-input, .el-button {
margin-top: 40px;
}
.el-button {
width: 100%;
font-size: 18px;
}
.foot > span {
float: right;
margin-top: 20px;
color: orange;
cursor: pointer;
}
.sms {
color: orange;
cursor: pointer;
display: inline-block;
width: 70px;
text-align: center;
user-select: none;
}
</style>
4.9 前臺驗證碼登陸
#詳細程式碼看第三條,前臺傳送驗證碼
5 註冊
5.1 後臺註冊介面
# urls.py
router.register('register', views.RegisterView, 'register') # /user/register post請求就是新增
# views.py
class RegisterView(GenericViewSet,CreateModelMixin):
queryset = models.User.objects.all()
serializer_class = serializer.UserRegisterSerilaizer
def create(self, request, *args, **kwargs):
response=super().create(request, *args, **kwargs)
username=response.data.get('username')
return APIResponse(code=1,msg='註冊成功',username=username)
# serializer.py
class UserRegisterSerilaizer(serializers.ModelSerializer):
code=serializers.CharField(max_length=4,min_length=4,write_only=True)
class Meta:
model = models.User
fields = ['telephone', 'code','password','username']
extra_kwargs = {
'password': {'max_length': 18,'min_length':8},
'username': {'read_only':True}
}
def validate(self, attrs):
telephone = attrs.get('telephone')
code = attrs.get('code')
# 取出原來的code
cache_code = cache.get(settings.PHONE_CACHE_KEY % telephone)
if code == cache_code:
# 驗證碼通過
if re.match('^1[3-9][0-9]{9}$', telephone):
attrs['username']=telephone # 把使用者的名字設成手機號
attrs.pop('code')
return attrs
else:
raise ValidationError('手機號不合法')
else:
raise ValidationError('驗證碼錯誤')
# 重寫create方法
def create(self, validated_data):
user=models.User.objects.create_user(**validated_data)
return user
5.2 vue註冊元件
# Register.vue
<template>
<div class="register">
<div class="box">
<i class="el-icon-close" @click="close_register"></i>
<div class="content">
<div class="nav">
<span class="active">新使用者註冊</span>
</div>
<el-form>
<el-input
placeholder="手機號"
prefix-icon="el-icon-phone-outline"
v-model="mobile"
clearable
@blur="check_mobile">
</el-input>
<el-input
placeholder="密碼"
prefix-icon="el-icon-key"
v-model="password"
clearable
show-password>
</el-input>
<el-input
placeholder="驗證碼"
prefix-icon="el-icon-chat-line-round"
v-model="sms"
clearable>
<template slot="append">
<span class="sms" @click="send_sms">{{ sms_interval }}</span>
</template>
</el-input>
<el-button type="primary" @click="register">註冊</el-button>
</el-form>
<div class="foot">
<span @click="go_login">立即登入</span>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: "Register",
data() {
return {
mobile: '',
password: '',
sms: '',
sms_interval: '獲取驗證碼',
is_send: false,
}
},
methods: {
close_register() {
this.$emit('close', false)
},
go_login() {
this.$emit('go')
},
check_mobile() {
if (!this.mobile) return;
//字串.match(/正則表示式/)
if (!this.mobile.match(/^1[3-9][0-9]{9}$/)) {
this.$message({
message: '手機號有誤',
type: 'warning',
duration: 1000,
onClose: () => {
this.mobile = '';
}
});
return false;
}
this.$axios.get(this.$settings.base_url + '/user/check_telephone/', {params: {telephone: this.mobile}}).then(response => {
if (response.data.code) {
this.$message({
message: '您已經註冊過了,快去登入把',
type: 'warning',
duration: 1000,
onClose: () => {
this.go_login()
}
});
} else {
this.is_send = true;
this.$message({
message: '該使用者沒有註冊過,歡迎註冊我們的平臺',
type: 'success',
duration: 1000,
});
}
}).catch(error => {
console.log(error)
})
},
send_sms() {
if (!this.is_send) return;
this.is_send = false;
let sms_interval_time = 60;
this.sms_interval = "傳送中...";
this.$axios.get(this.$settings.base_url + '/user/send/', {params: {'telephone': this.mobile}})
.then(response => {
if (response.data.code) {
this.$message({
message: '傳送驗證碼成功',
type: 'success',
duration: 1000,
});
}
})
// setInterval(()=>{},100)
//定時器:每隔一秒種,把數字減一,當減到小於1,按鈕又能點了,顯示獲取驗證碼
let timer = setInterval(() => {
if (sms_interval_time <= 1) {
clearInterval(timer); //如果小於等於1,把定時器清除
this.sms_interval = "獲取驗證碼";
this.is_send = true; // 重新回覆點擊發送功能的條件
} else {
sms_interval_time -= 1;
this.sms_interval = `${sms_interval_time}秒後再發`;
}
}, 1000);
},
register() {
if (this.mobile && this.sms && this.password) {
this.$axios.post(this.$settings.base_url + '/user/register/', {
telephone: this.mobile,
code: this.sms,
password: this.password
}).then(response => {
if (response.data.code) {
//註冊成功,來個提示,跳轉到登入
this.$message({
message: '註冊成功',
type: 'success',
duration: 1000,
onClose: () => {
this.go_login()
}
});
} else {
this.$message({
message: '未知錯誤',
type: 'error',
duration: 1000,
onClose: () => {
this.mobile = ''
this.sms = ''
this.password = ''
}
});
}
})
} else {
this.$message({
message: '你有沒填的資訊',
type: 'error',
duration: 1000,
});
}
},
}
}
</script>
<style scoped>
.register {
width: 100vw;
height: 100vh;
position: fixed;
top: 0;
left: 0;
z-index: 10;
background-color: rgba(0, 0, 0, 0.3);
}
.box {
width: 400px;
height: 480px;
background-color: white;
border-radius: 10px;
position: relative;
top: calc(50vh - 240px);
left: calc(50vw - 200px);
}
.el-icon-close {
position: absolute;
font-weight: bold;
font-size: 20px;
top: 10px;
right: 10px;
cursor: pointer;
}
.el-icon-close:hover {
color: darkred;
}
.content {
position: absolute;
top: 40px;
width: 280px;
left: 60px;
}
.nav {
font-size: 20px;
height: 38px;
border-bottom: 2px solid darkgrey;
}
.nav > span {
margin-left: 90px;
color: darkgrey;
user-select: none;
cursor: pointer;
padding-bottom: 10px;
border-bottom: 2px solid darkgrey;
}
.nav > span.active {
color: black;
border-bottom: 3px solid black;
padding-bottom: 9px;
}
.el-input, .el-button {
margin-top: 40px;
}
.el-button {
width: 100%;
font-size: 18px;
}
.foot > span {
float: right;
margin-top: 20px;
color: orange;
cursor: pointer;
}
.sms {
color: orange;
cursor: pointer;
display: inline-block;
width: 70px;
text-align: center;
user-select: none;
}
</style>
6 課程
6.1 課程表結構分析和優化
ForeignKey的欄位:
-related_name 反向操作時,使用的欄位名,用於代替原反向查詢時的'表名_set'。
-related_query_name :反向查詢操作時,使用的連線字首,用於替換表名。
# luffyapi/utils/models.py
class BaseModel(models.Model):
is_delete = models.BooleanField(verbose_name='是否刪除',default=False)
created_time = models.DateTimeField(auto_now_add=True, verbose_name='建立時間')
updated_time = models.DateTimeField(auto_now=True, verbose_name='更新時間')
orders = models.IntegerField(verbose_name='播放順序')
is_show = models.BooleanField(verbose_name='是否展示')
class Meta:
abstract = True
# 1 course/models.py
from luffyapi.utils.models import BaseModel
# 路飛課程相關表,以免費課為例
class CourseCategory(BaseModel):
"""分類
python,linux,go, 網路安全
跟課程是一對多的關係
"""
name = models.CharField(max_length=64, unique=True, verbose_name="分類名稱")
class Meta:
db_table = "luffy_course_category"
verbose_name = "分類"
verbose_name_plural = verbose_name
def __str__(self):
return "%s" % self.name
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="courses", max_length=255, verbose_name="封面圖片", blank=True, null=True)
course_type = models.SmallIntegerField(choices=course_type, default=0, verbose_name="付費型別")
# 使用這個欄位的原因
brief = models.TextField(max_length=2048, verbose_name="詳情介紹", null=True, blank=True)
level = models.SmallIntegerField(choices=level_choices, default=0, verbose_name="難度等級")
pub_date = models.DateField(verbose_name="釋出日期", auto_now_add=True)
period = models.IntegerField(verbose_name="建議學習週期(day)", default=7)
attachment_path = models.FileField(upload_to="attachment", max_length=128, verbose_name="課件路徑", blank=True, null=True)
status = models.SmallIntegerField(choices=status_choices, default=0, verbose_name="課程狀態")
price = models.DecimalField(max_digits=6, decimal_places=2, verbose_name="課程原價", default=0)
# 優化欄位
students = models.IntegerField(verbose_name="學習人數", default=0)
sections = models.IntegerField(verbose_name="總課時數量", default=0)
pub_sections = models.IntegerField(verbose_name="課時更新數量", default=0)
# 關聯欄位
teacher = models.ForeignKey("Teacher", on_delete=models.DO_NOTHING, null=True, blank=True, verbose_name="授課老師",db_constraint=False)
course_category = models.ForeignKey("CourseCategory", on_delete=models.SET_NULL, db_constraint=False, null=True, blank=True,verbose_name="課程分類")
class Meta:
db_table = "luffy_course"
verbose_name = "課程"
verbose_name_plural = "課程"
def __str__(self):
return "%s" % self.name
class Teacher(BaseModel):
"""導師
跟課程一對多,關聯欄位寫在課程表中
"""
role_choices = (
(0, '講師'),
(1, '導師'),
(2, '班主任'),
)
name = models.CharField(max_length=32, verbose_name="導師名")
role = models.SmallIntegerField(choices=role_choices, default=0, verbose_name="導師身份")
title = models.CharField(max_length=64, verbose_name="職位、職稱")
signature = models.CharField(max_length=255, verbose_name="導師簽名", help_text="導師簽名", blank=True, null=True)
image = models.ImageField(upload_to="teacher", null=True, verbose_name="導師封面")
brief = models.TextField(max_length=1024, verbose_name="導師描述")
class Meta:
db_table = "luffy_teacher"
verbose_name = "導師"
verbose_name_plural = verbose_name
def __str__(self):
return "%s" % self.name
class CourseChapter(BaseModel):
"""章節
章節跟課程是一(課程)對多(章節多)
"""
course = models.ForeignKey("Course", related_name='coursechapters', on_delete=models.CASCADE, verbose_name="課程名稱",db_constraint=False)
chapter = models.SmallIntegerField(verbose_name="第幾章", default=1)
name = models.CharField(max_length=128, verbose_name="章節標題")
summary = models.TextField(verbose_name="章節介紹", blank=True, null=True)
pub_date = models.DateField(verbose_name="釋出日期", auto_now_add=True)
class Meta:
db_table = "luffy_course_chapter"
verbose_name = "章節"
verbose_name_plural = verbose_name
def __str__(self):
return "%s:(第%s章)%s" % (self.course, self.chapter, self.name)
class CourseSection(BaseModel):
"""課時
章節和課時是一對多的關係,關聯欄位寫在多的一方,課時
"""
section_type_choices = (
(0, '文件'),
(1, '練習'),
(2, '視訊')
)
chapter = models.ForeignKey("CourseChapter", related_name='coursesections', on_delete=models.CASCADE,
verbose_name="課程章節",db_constraint=False)
name = models.CharField(max_length=128, verbose_name="課時標題")
orders = models.PositiveSmallIntegerField(verbose_name="課時排序")
section_type = models.SmallIntegerField(default=2, choices=section_type_choices, verbose_name="課時種類")
section_link = models.CharField(max_length=255, blank=True, null=True, verbose_name="課時連結",
help_text="若是video,填vid,若是文件,填link")
duration = models.CharField(verbose_name="視訊時長", blank=True, null=True, max_length=32) # 僅在前端展示使用
pub_date = models.DateTimeField(verbose_name="釋出時間", auto_now_add=True)
free_trail = models.BooleanField(verbose_name="是否可試看", default=False)
class Meta:
db_table = "luffy_course_Section"
verbose_name = "課時"
verbose_name_plural = verbose_name
def __str__(self):
return "%s-%s" % (self.chapter, self.name)
# 2 utils/models/BaseModel
from django.db import models
# 後期課程表,輪播圖表,都會用到這些欄位
class BaseModel(models.Model):
is_delete = models.BooleanField(default=False, verbose_name='是否刪除')
is_show = models.BooleanField(default=True, verbose_name='是否展示')
# 修改成這樣
created_time = models.DateTimeField(auto_now_add=True, verbose_name='建立時間')
updated_time = models.DateTimeField(auto_now=True, verbose_name='最後更新時間')
orders = models.IntegerField()
class Meta:
abstract=True # 一定不要忘了
# 3 資料遷移
python manage.py makemigrations
python manage.py migrate
6.2 課程主頁vue頁面雛形
# 不要忘了註冊路由
# FreeCourse.vue
<template>
<div class="course">
<Header></Header>
<div class="main">
<!-- 篩選條件 -->
<div class="condition">
<ul class="cate-list">
<li class="title">課程分類:</li>
<li class="this">全部</li>
<li>Python</li>
<li>Linux運維</li>
<li>Python進階</li>
<li>開發工具</li>
<li>Go語言</li>
<li>機器學習</li>
<li>技術生涯</li>
</ul>
<div class="ordering">
<ul>
<li class="title">篩 選:</li>
<li class="default this">預設</li>
<li class="hot">人氣</li>
<li class="price">價格</li>
</ul>
<p class="condition-result">共21個課程</p>
</div>
</div>
<!-- 課程列表 -->
<div class="course-list">
<div class="course-item">
<div class="course-image">
<img src="@/assets/img/course-cover.jpeg" alt="">
</div>
<div class="course-info">
<h3>Python開發21天入門 <span><img src="@/assets/img/avatar1.svg" alt="">100人已加入學習</span></h3>
<p class="teather-info">Alex 金角大王 老男孩Python教學總監 <span>共154課時/更新完成</span></p>
<ul class="lesson-list">
<li><span class="lesson-title">01 | 第1節:初識編碼</span> <span class="free">免費</span></li>
<li><span class="lesson-title">01 | 第1節:初識編碼初識編碼</span> <span class="free">免費</span></li>
<li><span class="lesson-title">01 | 第1節:初識編碼</span></li>
<li><span class="lesson-title">01 | 第1節:初識編碼初識編碼</span></li>
</ul>
<div class="pay-box">
<span class="discount-type">限時免費</span>
<span class="discount-price">¥0.00元</span>
<span class="original-price">原價:9.00元</span>
<span class="buy-now">立即購買</span>
</div>
</div>
</div>
<div class="course-item">
<div class="course-image">
<img src="@/assets/img/course-cover.jpeg" alt="">
</div>
<div class="course-info">
<h3>Python開發21天入門 <span><img src="@/assets/img/avatar1.svg" alt="">100人已加入學習</span></h3>
<p class="teather-info">Alex 金角大王 老男孩Python教學總監 <span>共154課時/更新完成</span></p>
<ul class="lesson-list">
<li><span class="lesson-title">01 | 第1節:初識編碼</span> <span class="free">免費</span></li>
<li><span class="lesson-title">01 | 第1節:初識編碼初識編碼</span> <span class="free">免費</span></li>
<li><span class="lesson-title">01 | 第1節:初識編碼</span></li>
<li><span class="lesson-title">01 | 第1節:初識編碼初識編碼</span></li>
</ul>
<div class="pay-box">
<span class="discount-type">限時免費</span>
<span class="discount-price">¥0.00元</span>
<span class="original-price">原價:9.00元</span>
<span class="buy-now">立即購買</span>
</div>
</div>
</div>
<div class="course-item">
<div class="course-image">
<img src="@/assets/img/course-cover.jpeg" alt="">
</div>
<div class="course-info">
<h3>Python開發21天入門 <span><img src="@/assets/img/avatar1.svg" alt="">100人已加入學習</span></h3>
<p class="teather-info">Alex 金角大王 老男孩Python教學總監 <span>共154課時/更新完成</span></p>
<ul class="lesson-list">
<li><span class="lesson-title">01 | 第1節:初識編碼</span> <span class="free">免費</span></li>
<li><span class="lesson-title">01 | 第1節:初識編碼初識編碼</span> <span class="free">免費</span></li>
<li><span class="lesson-title">01 | 第1節:初識編碼</span></li>
<li><span class="lesson-title">01 | 第1節:初識編碼初識編碼</span></li>
</ul>
<div class="pay-box">
<span class="discount-type">限時免費</span>
<span class="discount-price">¥0.00元</span>
<span class="original-price">原價:9.00元</span>
<span class="buy-now">立即購買</span>
</div>
</div>
</div>
<div class="course-item">
<div class="course-image">
<img src="@/assets/img/course-cover.jpeg" alt="">
</div>
<div class="course-info">
<h3>Python開發21天入門 <span><img src="@/assets/img/avatar1.svg" alt="">100人已加入學習</span></h3>
<p class="teather-info">Alex 金角大王 老男孩Python教學總監 <span>共154課時/更新完成</span></p>
<ul class="lesson-list">
<li><span class="lesson-title">01 | 第1節:初識編碼</span> <span class="free">免費</span></li>
<li><span class="lesson-title">01 | 第1節:初識編碼初識編碼</span> <span class="free">免費</span></li>
<li><span class="lesson-title">01 | 第1節:初識編碼</span></li>
<li><span class="lesson-title">01 | 第1節:初識編碼初識編碼</span></li>
</ul>
<div class="pay-box">
<span class="discount-type">限時免費</span>
<span class="discount-price">¥0.00元</span>
<span class="original-price">原價:9.00元</span>
<span class="buy-now">立即購買</span>
</div>
</div>
</div>
</div>
</div>
<Footer></Footer>
</div>
</template>
<script>
import Header from "@/components/Head"
import Footer from "@/components/Footer"
export default {
name: "Course",
data() {
return {
category: 0,
}
},
components: {
Header,
Footer,
}
}
</script>
<style scoped>
.course {
background: #f6f6f6;
}
.course .main {
width: 1100px;
margin: 35px auto 0;
}
.course .condition {
margin-bottom: 35px;
padding: 25px 30px 25px 20px;
background: #fff;
border-radius: 4px;
box-shadow: 0 2px 4px 0 #f0f0f0;
}
.course .cate-list {
border-bottom: 1px solid #333;
border-bottom-color: rgba(51, 51, 51, .05);
padding-bottom: 18px;
margin-bottom: 17px;
}
.course .cate-list::after {
content: "";
display: block;
clear: both;
}
.course .cate-list li {
float: left;
font-size: 16px;
padding: 6px 15px;
line-height: 16px;
margin-left: 14px;
position: relative;
transition: all .3s ease;
cursor: pointer;
color: #4a4a4a;
border: 1px solid transparent; /* transparent 透明 */
}
.course .cate-list .title {
color: #888;
margin-left: 0;
letter-spacing: .36px;
padding: 0;
line-height: 28px;
}
.course .cate-list .this {
color: #ffc210;
border: 1px solid #ffc210 !important;
border-radius: 30px;
}
.course .ordering::after {
content: "";
display: block;
clear: both;
}
.course .ordering ul {
float: left;
}
.course .ordering ul::after {
content: "";
display: block;
clear: both;
}
.course .ordering .condition-result {
float: right;
font-size: 14px;
color: #9b9b9b;
line-height: 28px;
}
.course .ordering ul li {
float: left;
padding: 6px 15px;
line-height: 16px;
margin-left: 14px;
position: relative;
transition: all .3s ease;
cursor: pointer;
color: #4a4a4a;
}
.course .ordering .title {
font-size: 16px;
color: #888;
letter-spacing: .36px;
margin-left: 0;
padding: 0;
line-height: 28px;
}
.course .ordering .this {
color: #ffc210;
}
.course .ordering .price {
position: relative;
}
.course .ordering .price::before,
.course .ordering .price::after {
cursor: pointer;
content: "";
display: block;
width: 0px;
height: 0px;
border: 5px solid transparent;
position: absolute;
right: 0;
}
.course .ordering .price::before {
border-bottom: 5px solid #aaa;
margin-bottom: 2px;
top: 2px;
}
.course .ordering .price::after {
border-top: 5px solid #aaa;
bottom: 2px;
}
.course .course-item:hover {
box-shadow: 4px 6px 16px rgba(0, 0, 0, .5);
}
.course .course-item {
width: 1100px;
background: #fff;
padding: 20px 30px 20px 20px;
margin-bottom: 35px;
border-radius: 2px;
cursor: pointer;
box-shadow: 2px 3px 16px rgba(0, 0, 0, .1);
/* css3.0 過渡動畫 hover 事件操作 */
transition: all .2s ease;
}
.course .course-item::after {
content: "";
display: block;
clear: both;
}
/* 頂級元素 父級元素 當前元素{} */
.course .course-item .course-image {
float: left;
width: 423px;
height: 210px;
margin-right: 30px;
}
.course .course-item .course-image img {
width: 100%;
}
.course .course-item .course-info {
float: left;
width: 596px;
}
.course-item .course-info h3 {
font-size: 26px;
color: #333;
font-weight: normal;
margin-bottom: 8px;
}
.course-item .course-info h3 span {
font-size: 14px;
color: #9b9b9b;
float: right;
margin-top: 14px;
}
.course-item .course-info h3 span img {
width: 11px;
height: auto;
margin-right: 7px;
}
.course-item .course-info .teather-info {
font-size: 14px;
color: #9b9b9b;
margin-bottom: 14px;
padding-bottom: 14px;
border-bottom: 1px solid #333;
border-bottom-color: rgba(51, 51, 51, .05);
}
.course-item .course-info .teather-info span {
float: right;
}
.course-item .lesson-list::after {
content: "";
display: block;
clear: both;
}
.course-item .lesson-list li {
float: left;
width: 44%;
font-size: 14px;
color: #666;
padding-left: 22px;
/* background: url("路徑") 是否平鋪 x軸位置 y軸位置 */
background: url("/src/assets/img/play-icon-gray.svg") no-repeat left 4px;
margin-bottom: 15px;
}
.course-item .lesson-list li .lesson-title {
/* 以下3句,文字內容過多,會自動隱藏,並顯示省略符號 */
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
display: inline-block;
max-width: 200px;
}
.course-item .lesson-list li:hover {
background-image: url("/src/assets/img/play-icon-yellow.svg");
color: #ffc210;
}
.course-item .lesson-list li .free {
width: 34px;
height: 20px;
color: #fd7b4d;
vertical-align: super;
margin-left: 10px;
border: 1px solid #fd7b4d;
border-radius: 2px;
text-align: center;
font-size: 13px;
white-space: nowrap;
}
.course-item .lesson-list li:hover .free {
color: #ffc210;
border-color: #ffc210;
}
.course-item .pay-box::after {
content: "";
display: block;
clear: both;
}
.course-item .pay-box .discount-type {
padding: 6px 10px;
font-size: 16px;
color: #fff;
text-align: center;
margin-right: 8px;
background: #fa6240;
border: 1px solid #fa6240;
border-radius: 10px 0 10px 0;
float: left;
}
.course-item .pay-box .discount-price {
font-size: 24px;
color: #fa6240;
float: left;
}
.course-item .pay-box .original-price {
text-decoration: line-through;
font-size: 14px;
color: #9b9b9b;
margin-left: 10px;
float: left;
margin-top: 10px;
}
.course-item .pay-box .buy-now {
width: 120px;
height: 38px;
background: transparent;
color: #fa6240;
font-size: 16px;
border: 1px solid #fd7b4d;
border-radius: 3px;
transition: all .2s ease-in-out;
float: right;
text-align: center;
line-height: 38px;
}
.course-item .pay-box .buy-now:hover {
color: #fff;
background: #ffc210;
border: 1px solid #ffc210;
}
</style>
6.3 課程資料錄入
-- 老師表
INSERT INTO luffy_teacher(id, orders, is_show, is_delete, created_time, updated_time, name, role, title, signature, image, brief) VALUES (1, 1, 1, 0, '2019-07-14 13:44:19.661327', '2019-07-14 13:46:54.246271', 'Alex', 1, '老男孩Python教學總監', '金角大王', 'teacher/alex_icon.png', '老男孩教育CTO & CO-FOUNDER 國內知名PYTHON語言推廣者 51CTO學院2016\2017年度最受學員喜愛10大講師之一 多款開源軟體作者 曾任職公安部、飛信、中金公司、NOKIA中國研究院、華爾街英語、ADVENT、汽車之家等公司');
INSERT INTO luffy_teacher(id, orders, is_show, is_delete, created_time, updated_time, name, role, title, signature, image, brief) VALUES (2, 2, 1, 0, '2019-07-14 13:45:25.092902', '2019-07-14 13:45:25.092936', 'Mjj', 0, '前美團前端專案組架構師', NULL, 'teacher/mjj_icon.png', '是馬JJ老師, 一個集美貌與才華於一身的男人,搞過幾年IOS,又轉了前端開發幾年,曾就職於美團網任高階前端開發,後來因為不同意王興(美團老闆)的戰略佈局而出家做老師去了,有豐富的教學經驗,開起車來也毫不含糊。一直專注在前端的前沿技術領域。同時,愛好抽菸、喝酒、燙頭(錫紙燙)。 我的最愛是前端,因為前端妹子多。');
INSERT INTO luffy_teacher(id, orders, is_show, is_delete, created_time, updated_time, name, role, title, signature, image, brief) VALUES (3, 3, 1, 0, '2019-07-14 13:46:21.997846', '2019-07-14 13:46:21.997880', 'Lyy', 0, '老男孩Linux學科帶頭人', NULL, 'teacher/lyy_icon.png', 'Linux運維技術專家,老男孩Linux金牌講師,講課風趣幽默、深入淺出、聲音洪亮到爆炸');
-- 課程分類表
INSERT INTO luffy_course_category(id, orders, is_show, is_delete, created_time, updated_time, name) VALUES (1, 1, 1, 0, '2019-07-14 13:40:58.690413', '2019-07-14 13:40:58.690477', 'Python');
INSERT INTO luffy_course_category(id, orders, is_show, is_delete, created_time, updated_time, name) VALUES (2, 2, 1, 0, '2019-07-14 13:41:08.249735', '2019-07-14 13:41:08.249817', 'Linux');
-- 課程表資料
INSERT INTO luffy_course(id, orders, is_show, is_delete, created_time, updated_time, name, course_img, course_type, brief, level, pub_date, period, attachment_path, status, students, sections, pub_sections, price, course_category_id, teacher_id) VALUES (1, 1, 1, 0, '2019-07-14 13:54:33.095201', '2019-07-14 13:54:33.095238', 'Python開發21天入門', 'courses/alex_python.png', 0, 'Python從入門到入土&&&Python從入門到入土&&&Python從入門到入土&&&Python從入門到入土&&&Python從入門到入土&&&Python從入門到入土&&&Python從入門到入土&&&Python從入門到入土&&&Python從入門到入土&&&Python從入門到入土&&&Python從入門到入土&&&Python從入門到入土', 0, '2019-07-14', 21, '', 0, 231, 120, 120, 0.00, 1, 1);
INSERT INTO luffy_course(id, orders, is_show, is_delete, created_time, updated_time, name, course_img, course_type, brief, level, pub_date, period, attachment_path, status, students, sections, pub_sections, price, course_category_id, teacher_id) VALUES (2, 2, 1, 0, '2019-07-14 13:56:05.051103', '2019-07-14 13:56:05.051142', 'Python專案實戰', 'courses/mjj_python.png', 0, '', 1, '2019-07-14', 30, '', 0, 340, 120, 120, 99.00, 1, 2);
INSERT INTO luffy_course(id, orders, is_show, is_delete, created_time, updated_time, name, course_img, course_type, brief, level, pub_date, period, attachment_path, status, students, sections, pub_sections, price, course_category_id, teacher_id) VALUES (3, 3, 1, 0, '2019-07-14 13:57:21.190053', '2019-07-14 13:57:21.190095', 'Linux系統基礎5周入門精講', 'courses/lyy_linux.png', 0, '', 0, '2019-07-14', 25, '', 0, 219, 100, 100, 39.00, 2, 3);
-- 章節表資料
INSERT INTO luffy_course_chapter(id, orders, is_show, is_delete, created_time, updated_time, chapter, name, summary, pub_date, course_id) VALUES (1, 1, 1, 0, '2019-07-14 13:58:34.867005', '2019-07-14 14:00:58.276541', 1, '計算機原理', '', '2019-07-14', 1);
INSERT INTO luffy_course_chapter(id, orders, is_show, is_delete, created_time, updated_time, chapter, name, summary, pub_date, course_id) VALUES (2, 2, 1, 0, '2019-07-14 13:58:48.051543', '2019-07-14 14:01:22.024206', 2, '環境搭建', '', '2019-07-14', 1);
INSERT INTO luffy_course_chapter(id, orders, is_show, is_delete, created_time, updated_time, chapter, name, summary, pub_date, course_id) VALUES (3, 3, 1, 0, '2019-07-14 13:59:09.878183', '2019-07-14 14:01:40.048608', 1, '專案建立', '', '2019-07-14', 2);
INSERT INTO luffy_course_chapter(id, orders, is_show, is_delete, created_time, updated_time, chapter, name, summary, pub_date, course_id) VALUES (4, 4, 1, 0, '2019-07-14 13:59:37.448626', '2019-07-14 14:01:58.709652', 1, 'Linux環境建立', '', '2019-07-14', 3);
-- 課時表資料
INSERT INTO luffy_course_Section(id, is_show, is_delete, created_time, updated_time, name, orders, section_type, section_link, duration, pub_date, free_trail, chapter_id) VALUES (1, 1, 0, '2019-07-14 14:02:33.779098', '2019-07-14 14:02:33.779135', '計算機原理上', 1, 2, NULL, NULL, '2019-07-14 14:02:33.779193', 1, 1);
INSERT INTO luffy_course_Section(id, is_show, is_delete, created_time, updated_time, name, orders, section_type, section_link, duration, pub_date, free_trail, chapter_id) VALUES (2, 1, 0, '2019-07-14 14:02:56.657134', '2019-07-14 14:02:56.657173', '計算機原理下', 2, 2, NULL, NULL, '2019-07-14 14:02:56.657227', 1, 1);
INSERT INTO luffy_course_Section(id, is_show, is_delete, created_time, updated_time, name, orders, section_type, section_link, duration, pub_date, free_trail, chapter_id) VALUES (3, 1, 0, '2019-07-14 14:03:20.493324', '2019-07-14 14:03:52.329394', '環境搭建上', 1, 2, NULL, NULL, '2019-07-14 14:03:20.493420', 0, 2);
INSERT INTO luffy_course_Section(id, is_show, is_delete, created_time, updated_time, name, orders, section_type, section_link, duration, pub_date, free_trail, chapter_id) VALUES (4, 1, 0, '2019-07-14 14:03:36.472742', '2019-07-14 14:03:36.472779', '環境搭建下', 2, 2, NULL, NULL, '2019-07-14 14:03:36.472831', 0, 2);
INSERT INTO luffy_course_Section(id, is_show, is_delete, created_time, updated_time, name, orders, section_type, section_link, duration, pub_date, free_trail, chapter_id) VALUES (5, 1, 0, '2019-07-14 14:04:19.338153', '2019-07-14 14:04:19.338192', 'web專案的建立', 1, 2, NULL, NULL, '2019-07-14 14:04:19.338252', 1, 3);
INSERT INTO luffy_course_Section(id, is_show, is_delete, created_time, updated_time, name, orders, section_type, section_link, duration, pub_date, free_trail, chapter_id) VALUES (6, 1, 0, '2019-07-14 14:04:52.895855', '2019-07-14 14:04:52.895890', 'Linux的環境搭建', 1, 2, NULL, NULL, '2019-07-14 14:04:52.895942', 1, 4);
6.4 課程分類群查介面
# 路由urls.py
router = SimpleRouter()
router.register('categories', views.CourseCategoryView, 'categories')
router.register('free', views.CouresView, 'free')
urlpatterns = [
path('', include(router.urls)),
]
# 檢視views.py
from rest_framework.viewsets import GenericViewSet
from rest_framework.mixins import ListModelMixin
from . import models
from . import serializer
class CourseCategoryView(GenericViewSet,ListModelMixin):
queryset = models.CourseCategory.objects.filter(is_delete=False,is_show=True).order_by('orders')
serializer_class = serializer.CourseCategorySerializer
# 序列化類
from rest_framework import serializers
from . import models
class CourseCategorySerializer(serializers.ModelSerializer):
class Meta:
model=models.CourseCategory
fields=['id','name']
6.5 課程群查介面
# 路由
router = SimpleRouter()
router.register('categories', views.CourseCategoryView, 'categories')
router.register('free', views.CouresView, 'free')
urlpatterns = [
path('', include(router.urls)),
]
# 檢視 views.py
from .paginations import PageNumberPagination
from django_filters.rest_framework import DjangoFilterBackend
from rest_framework.filters import OrderingFilter,SearchFilter
from .filters import MyFilter
class CouresView(GenericViewSet,ListModelMixin,RetrieveModelMixin):
queryset = models.Course.objects.filter(is_delete=False,is_show=True).order_by('orders')
serializer_class = serializer.CourseModelSerializer
pagination_class = PageNumberPagination
filter_backends=[DjangoFilterBackend,OrderingFilter]
ordering_fields=['id', 'price', 'students']
filter_fields=['course_category','students']
# serializer
from rest_framework import serializers
from . import models
class CourseCategorySerializer(serializers.ModelSerializer):
class Meta:
model = models.CourseCategory
fields = ['id', 'name']
class TeacherSerializer(serializers.ModelSerializer):
class Meta:
model = models.Teacher
fields = ('name', 'role_name', 'title', 'signature', 'image', 'brief')
class CourseModelSerializer(serializers.ModelSerializer):
# 子序列化的方式
teacher = TeacherSerializer()
class Meta:
model = models.Course
fields=[
'id',
'name',
'course_img',
'brief',
'attachment_path',
'pub_sections',
'price',
'students',
'period',
'sections',
'course_type_name',
'level_name',
'status_name',
'teacher',
'section_list',
]
# models.py
class Course(BaseModel):
...
@property
def course_type_name(self):
return self.get_course_type_display()
@property
def level_name(self):
return self.get_level_display()
@property
def status_name(self):
return self.get_status_display()
@property
def section_list(self):
ll=[]
# 根據課程取出所有章節(正向查詢,欄位名.all())
course_chapter_list=self.coursechapters.all()
for course_chapter in course_chapter_list:
# 通過章節物件,取到章節下所有的課時(反向查詢)
# course_chapter.表名小寫_set.all() 現在變成了course_chapter.coursesections.all()
course_sections_list=course_chapter.coursesections.all()
for course_section in course_sections_list:
ll.append({
'name': course_section.name,
'section_link': course_section.section_link,
'duration': course_section.duration,
'free_trail': course_section.free_trail,
})
if len(ll)>=4:
return ll
return ll
class Teacher(BaseModel):
...
def role_name(self):
# 返回角色的中文
return self.get_role_display()
6.6 課程搜尋和過濾
排序:
按id正序倒敘排序,按price正序倒敘排列
使用:
http://127.0.0.1:8000/course/free/?ordering=-id
配置類:
filter_backends=[OrderingFilter]
配置欄位:
ordering_fields=['id','price']
內建過濾:
使用:
http://127.0.0.1:8000/course/free/?search=39
按照price過濾(表自有的欄位直接過濾)
配置類:
filter_backends=[SearchFilter]
配置欄位:
search_fields=['price']
擴充套件:django-filter
支援自由欄位的過濾還支援外來鍵欄位的過濾
安裝:
pip3 install django-filter
使用:
註冊,在專案settings.py的INSTALLED_APPS中註冊'django_filters'
http://127.0.0.1:8000/course/free/?course_category=1
# 過濾分類為1 (python的所有課程)
配置類:
filter_backends=[DjangoFilterBackend]
配置欄位:
filter_fields=['course_category']
6.7 實現區間過濾
# 藉助django-filter實現區間過濾
# 1 filters.py
class CourseFilterSet(FilterSet):
# 課程的價格範圍要大於min_price,小於max_price
min_price = filters.NumberFilter(field_name='price', lookup_expr='gt')
max_price = filters.NumberFilter(field_name='price', lookup_expr='lt')
class Meta:
model=models.Course
fields=['course_category']
# 2 檢視類中配置
filter_backends=[DjangoFilterBackend]
# 配置類:(自己寫的類)
filter_class = CourseFilterSet
6.8 課程主頁前端第二版
<template>
<div class="course">
<Header></Header>
<div class="main">
<!-- 篩選條件 -->
<div class="condition">
<ul class="cate-list">
<li class="title">課程分類:</li>
<li :class="filter.course_category==0?'this':''" @click="filter.course_category=0">全部</li>
<li :class="filter.course_category==category.id?'this':''" v-for="category in category_list"
@click="filter.course_category=category.id" :key="category.name">{{category.name}}
</li>
</ul>
<div class="ordering">
<ul>
<li class="title">篩 選:</li>
<li class="default" :class="(filter.ordering=='id' || filter.ordering=='-id')?'this':''"
@click="filter.ordering='-id'">預設
</li>
<li class="hot" :class="(filter.ordering=='students' || filter.ordering=='-students')?'this':''"
@click="filter.ordering=(filter.ordering=='-students'?'students':'-students')">人氣
</li>
<li class="price"
:class="filter.ordering=='price'?'price_up this':(filter.ordering=='-price'?'price_down this':'')"
@click="filter.ordering=(filter.ordering=='-price'?'price':'-price')">價格
</li>
</ul>
<p class="condition-result">共{{course_total}}個課程</p>
</div>
</div>
<!-- 課程列表 -->
<div class="course-list">
<div class="course-item" v-for="course in course_list" :key="course.name">
<div class="course-image">
<img :src="course.course_img" alt="">
</div>
<div class="course-info">
<h3>
<router-link :to="'/free/detail/'+course.id">{{course.name}}</router-link>
<span><img src="@/assets/img/avatar1.svg" alt="">{{course.students}}人已加入學習</span></h3>
<p class="teather-info">
{{course.teacher.name}} {{course.teacher.title}} {{course.teacher.signature}}
<span v-if="course.sections>course.pub_sections">共{{course.sections}}課時/已更新{{course.pub_sections}}課時</span>
<span v-else>共{{course.sections}}課時/更新完成</span>
</p>
<ul class="section-list">
<li v-for="(section, key) in course.section_list" :key="section.name"><span
class="section-title">0{{key+1}} | {{section.name}}</span>
<span class="free" v-if="section.free_trail">免費</span></li>
</ul>
<div class="pay-box">
<div v-if="course.discount_type">
<span class="discount-type">{{course.discount_type}}</span>
<span class="discount-price">¥{{course.real_price}}元</span>
<span class="original-price">原價:{{course.price}}元</span>
</div>
<span v-else class="discount-price">¥{{course.price}}元</span>
<span class="buy-now">立即購買</span>
</div>
</div>
</div>
</div>
<div class="course_pagination block">
<el-pagination
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page.sync="filter.page"
:page-sizes="[2, 3, 5, 10]"
:page-size="filter.page_size"
layout="sizes, prev, pager, next"
:total="course_total">
</el-pagination>
</div>
</div>
<Footer></Footer>
</div>
</template>
<script>
import Header from "@/components/Head"
import Footer from "@/components/Footer"
export default {
name: "Course",
data() {
return {
category_list: [], // 課程分類列表
course_list: [], // 課程列表
course_total: 0, // 當前課程的總數量
filter: {
course_category: 0, // 當前使用者選擇的課程分類,剛進入頁面預設為全部,值為0
ordering: "-id", // 資料的排序方式,預設值是-id,表示對於id進行降序排列
page_size: 2, // 單頁資料量
page: 1,
}
}
},
created() {
this.get_category();
this.get_course();
},
components: {
Header,
Footer,
},
watch: {
//當你監聽的資料發生變化,就會執行函式
"filter.course_category": function () {
this.filter.page = 1;
this.get_course();
},
"filter.ordering": function () {
this.get_course();
},
"filter.page_size": function () {
this.get_course();
},
"filter.page": function () {
this.get_course();
}
},
methods: {
handleSizeChange(val) {
// 每頁資料量發生變化時執行的方法
this.filter.page = 1;
this.filter.page_size = val;
},
handleCurrentChange(val) {
// 頁碼發生變化時執行的方法
this.filter.page = val;
},
get_category() {
// 獲取課程分類資訊
this.$axios.get(`${this.$settings.base_url}/course/categories/`).then(response => {
this.category_list = response.data;
}).catch(() => {
this.$message({
message: "獲取課程分類資訊有誤,請聯絡客服工作人員",
})
})
},
get_course() {
// 排序
let filters = {
ordering: this.filter.ordering, // 排序
};
// 判決是否進行分類課程的展示
if (this.filter.course_category > 0) {
filters.course_category = this.filter.course_category;
}
// 設定單頁資料量
if (this.filter.page_size > 0) {
filters.page_size = this.filter.page_size;
} else {
filters.page_size = 5;
}
// 設定當前頁碼
if (this.filter.page > 1) {
filters.page = this.filter.page;
} else {
filters.page = 1;
}
// 獲取課程列表資訊
this.$axios.get(`${this.$settings.base_url}/course/free/`, {
params: filters
}).then(response => {
// console.log(response.data);
this.course_list = response.data.results;
this.course_total = response.data.count;
// console.log(this.course_list);
}).catch(() => {
this.$message({
message: "獲取課程資訊有誤,請聯絡客服工作人員"
})
})
}
}
}
</script>
<style scoped>
.course {
background: #f6f6f6;
}
.course .main {
width: 1100px;
margin: 35px auto 0;
}
.course .condition {
margin-bottom: 35px;
padding: 25px 30px 25px 20px;
background: #fff;
border-radius: 4px;
box-shadow: 0 2px 4px 0 #f0f0f0;
}
.course .cate-list {
border-bottom: 1px solid #333;
border-bottom-color: rgba(51, 51, 51, .05);
padding-bottom: 18px;
margin-bottom: 17px;
}
.course .cate-list::after {
content: "";
display: block;
clear: both;
}
.course .cate-list li {
float: left;
font-size: 16px;
padding: 6px 15px;
line-height: 16px;
margin-left: 14px;
position: relative;
transition: all .3s ease;
cursor: pointer;
color: #4a4a4a;
border: 1px solid transparent; /* transparent 透明 */
}
.course .cate-list .title {
color: #888;
margin-left: 0;
letter-spacing: .36px;
padding: 0;
line-height: 28px;
}
.course .cate-list .this {
color: #ffc210;
border: 1px solid #ffc210 !important;
border-radius: 30px;
}
.course .ordering::after {
content: "";
display: block;
clear: both;
}
.course .ordering ul {
float: left;
}
.course .ordering ul::after {
content: "";
display: block;
clear: both;
}
.course .ordering .condition-result {
float: right;
font-size: 14px;
color: #9b9b9b;
line-height: 28px;
}
.course .ordering ul li {
float: left;
padding: 6px 15px;
line-height: 16px;
margin-left: 14px;
position: relative;
transition: all .3s ease;
cursor: pointer;
color: #4a4a4a;
}
.course .ordering .title {
font-size: 16px;
color: #888;
letter-spacing: .36px;
margin-left: 0;
padding: 0;
line-height: 28px;
}
.course .ordering .this {
color: #ffc210;
}
.course .ordering .price {
position: relative;
}
.course .ordering .price::before,
.course .ordering .price::after {
cursor: pointer;
content: "";
display: block;
width: 0px;
height: 0px;
border: 5px solid transparent;
position: absolute;
right: 0;
}
.course .ordering .price::before {
border-bottom: 5px solid #aaa;
margin-bottom: 2px;
top: 2px;
}
.course .ordering .price::after {
border-top: 5px solid #aaa;
bottom: 2px;
}
.course .ordering .price_up::before {
border-bottom-color: #ffc210;
}
.course .ordering .price_down::after {
border-top-color: #ffc210;
}
.course .course-item:hover {
box-shadow: 4px 6px 16px rgba(0, 0, 0, .5);
}
.course .course-item {
width: 1100px;
background: #fff;
padding: 20px 30px 20px 20px;
margin-bottom: 35px;
border-radius: 2px;
cursor: pointer;
box-shadow: 2px 3px 16px rgba(0, 0, 0, .1);
/* css3.0 過渡動畫 hover 事件操作 */
transition: all .2s ease;
}
.course .course-item::after {
content: "";
display: block;
clear: both;
}
/* 頂級元素 父級元素 當前元素{} */
.course .course-item .course-image {
float: left;
width: 423px;
height: 210px;
margin-right: 30px;
}
.course .course-item .course-image img {
max-width: 100%;
max-height: 210px;
}
.course .course-item .course-info {
float: left;
width: 596px;
}
.course-item .course-info h3 a {
font-size: 26px;
color: #333;
font-weight: normal;
margin-bottom: 8px;
}
.course-item .course-info h3 span {
font-size: 14px;
color: #9b9b9b;
float: right;
margin-top: 14px;
}
.course-item .course-info h3 span img {
width: 11px;
height: auto;
margin-right: 7px;
}
.course-item .course-info .teather-info {
font-size: 14px;
color: #9b9b9b;
margin-bottom: 14px;
padding-bottom: 14px;
border-bottom: 1px solid #333;
border-bottom-color: rgba(51, 51, 51, .05);
}
.course-item .course-info .teather-info span {
float: right;
}
.course-item .section-list::after {
content: "";
display: block;
clear: both;
}
.course-item .section-list li {
float: left;
width: 44%;
font-size: 14px;
color: #666;
padding-left: 22px;
/* background: url("路徑") 是否平鋪 x軸位置 y軸位置 */
background: url("/src/assets/img/play-icon-gray.svg") no-repeat left 4px;
margin-bottom: 15px;
}
.course-item .section-list li .section-title {
/* 以下3句,文字內容過多,會自動隱藏,並顯示省略符號 */
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
display: inline-block;
max-width: 200px;
}
.course-item .section-list li:hover {
background-image: url("/src/assets/img/play-icon-yellow.svg");
color: #ffc210;
}
.course-item .section-list li .free {
width: 34px;
height: 20px;
color: #fd7b4d;
vertical-align: super;
margin-left: 10px;
border: 1px solid #fd7b4d;
border-radius: 2px;
text-align: center;
font-size: 13px;
white-space: nowrap;
}
.course-item .section-list li:hover .free {
color: #ffc210;
border-color: #ffc210;
}
.course-item {
position: relative;
}
.course-item .pay-box {
position: absolute;
bottom: 20px;
width: 600px;
}
.course-item .pay-box::after {
content: "";
display: block;
clear: both;
}
.course-item .pay-box .discount-type {
padding: 6px 10px;
font-size: 16px;
color: #fff;
text-align: center;
margin-right: 8px;
background: #fa6240;
border: 1px solid #fa6240;
border-radius: 10px 0 10px 0;
float: left;
}
.course-item .pay-box .discount-price {
font-size: 24px;
color: #fa6240;
float: left;
}
.course-item .pay-box .original-price {
text-decoration: line-through;
font-size: 14px;
color: #9b9b9b;
margin-left: 10px;
float: left;
margin-top: 10px;
}
.course-item .pay-box .buy-now {
width: 120px;
height: 38px;
background: transparent;
color: #fa6240;
font-size: 16px;
border: 1px solid #fd7b4d;
border-radius: 3px;
transition: all .2s ease-in-out;
float: right;
text-align: center;
line-height: 38px;
position: absolute;
right: 0;
bottom: 5px;
}
.course-item .pay-box .buy-now:hover {
color: #fff;
background: #ffc210;
border: 1px solid #ffc210;
}
.course .course_pagination {
margin-bottom: 60px;
text-align: center;
}
</style>
6.9 課程詳情頁前端頁面
# 1 新建FreeCourseDetail.vue
# 2 配置路由
{
path: '/free/detail/:pk',
name: 'FreeCourseDetail',
component: FreeCourseDetail
},
#3 安裝 vue-video播放器
-cnpm install vue-video-player
-在main.js中配置
// vue-video播放器
require('video.js/dist/video-js.css');
require('vue-video-player/src/custom-theme.css');
import VideoPlayer from 'vue-video-player'
Vue.use(VideoPlayer);
#4 元件
<template>
<div class="detail">
<Header/>
<div class="main">
<div class="course-info">
<div class="wrap-left">
<videoPlayer class="video-player vjs-custom-skin"
ref="videoPlayer"
:playsinline="true"
:options="playerOptions"
@play="onPlayerPlay($event)"
@pause="onPlayerPause($event)">
</videoPlayer>
</div>
<div class="wrap-right">
<h3 class="course-name">{{course_info.name}}</h3>
<p class="data">{{course_info.students}}人在學 課程總時長:{{course_info.sections}}課時/{{course_info.pub_sections}}小時 難度:{{course_info.level_name}}</p>
<div 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="@/assets/img/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">使用者評論</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.sections}}個課時</p>
</div>
<div class="chapter-item" v-for="chapter in course_chapters" :key="chapter.name">
<p class="chapter-title"><img src="@/assets/img/enum.svg" alt="">第{{chapter.chapter}}章·{{chapter.name}}
</p>
<ul class="section-list">
<li class="section-item" v-for="section in chapter.coursesections" :key="section.name">
<p class="name"><span class="index">{{chapter.chapter}}-{{section.orders}}</span>
{{section.name}}<span class="free" v-if="section.free_trail">免費</span></p>
<p class="time">{{section.duration}} <img src="@/assets/img/chapter-player.svg"></p>
<button class="try" v-if="section.free_trail">立即試學</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 "@/components/Head"
import Footer from "@/components/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: {
aspectRatio: '16:9', // 將播放器置於流暢模式,並在計算播放器的動態大小時使用該值。值應該代表一個比例 - 用冒號分隔的兩個數字(例如"16:9"或"4:3")
sources: [{ // 播放資源和資源格式
type: "video/mp4",
src: "http://qe5smxg12.bkt.clouddn.com/3%20%E9%A6%96%E9%A1%B5%E8%BD%AE%E6%92%AD%E5%9B%BE%E5%BC%82%E6%AD%A5%E6%9B%B4%E6%96%B0.mp4" //你的視訊地址(必填)
}],
}
}
},
created() {
this.get_course_id();
this.get_course_data();
this.get_chapter();
},
methods: {
onPlayerPlay() {
// 當視訊播放時,執行的方法
console.log('視訊開始播放')
//加廣告
},
onPlayerPause() {
// 當視訊暫停播放時,執行的方法
console.log('視訊暫停,可以開啟廣告了')
//蓋張圖片
},
get_course_id() {
// 獲取位址列上面的課程ID
this.course_id = this.$route.params.pk || this.$route.query.pk;
if (this.course_id < 1) {
let _this = this;
_this.$alert("對不起,當前視訊不存在!", "警告", {
callback() {
_this.$router.go(-1);
}
});
}
},
get_course_data() {
// ajax請求課程資訊
this.$axios.get(`${this.$settings.base_url}/course/free/${this.course_id}/`).then(response => {
// window.console.log(response.data);
this.course_info = response.data;
console.log(this.course_info)
}).catch(() => {
this.$message({
message: "對不起,訪問頁面出錯!請聯絡客服工作人員!"
});
})
},
get_chapter() {
// 獲取當前課程對應的章節課時資訊
// http://127.0.0.1:8000/course/chapters/?course=(pk)
this.$axios.get(`${this.$settings.base_url}/course/chapters/`, {
params: {
"course": this.course_id,
}
}).then(response => {
this.course_chapters = response.data;
}).catch(error => {
window.console.log(error.response);
})
},
},
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-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;
}
.section-list {
padding: 0 20px;
}
.section-list .section-item {
padding: 15px 20px 15px 36px;
cursor: pointer;
justify-content: space-between;
position: relative;
overflow: hidden;
}
.section-item .name {
font-size: 14px;
color: #666;
float: left;
}
.section-item .index {
margin-right: 5px;
}
.section-item .free {
font-size: 12px;
color: #fff;
letter-spacing: .19px;
background: #ffc210;
border-radius: 100px;
padding: 1px 9px;
margin-left: 10px;
}
.section-item .time {
font-size: 14px;
color: #666;
letter-spacing: .23px;
opacity: 1;
transition: all .15s ease-in-out;
float: right;
}
.section-item .time img {
width: 18px;
height: 18px;
margin-left: 15px;
vertical-align: text-bottom;
}
.section-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;
}
.section-item:hover {
background: #fcf7ef;
box-shadow: 0 0 0 0 #f3f3f3;
}
.section-item:hover .name {
color: #333;
}
.section-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>
6.10 課程單查介面
class CouresView(GenericViewSet,ListModelMixin,RetrieveModelMixin):
queryset = models.Course.objects.filter(is_delete=False,is_show=True).order_by('orders')
serializer_class = serializer.CourseModelSerializer
pagination_class = PageNumberPagination
# 過濾和排序
# filter_backends=[DjangoFilterBackend,OrderingFilter,SearchFilter]
# filter_backends=[DjangoFilterBackend,OrderingFilter,MyFilter]
filter_backends=[DjangoFilterBackend,OrderingFilter]
# # filter_backends=OrderingFilter
ordering_fields=['id', 'price', 'students']
# # search_fields=['course_category']
filter_fields=['course_category']
6.11 章節分類介面
#1 urls.py
router.register('chapters', views.CourseChapterView, 'coursechapter')
# 2 views.py
class CourseChapterView(GenericViewSet,ListModelMixin):
queryset = models.CourseChapter.objects.filter(is_delete=False,is_show=True)
serializer_class = serializer.CourseChapterSerializer
# 可以按照課程id來查
filter_backends = [DjangoFilterBackend]
filter_fields = ['course']
# 3 serializer.py
class CourseSectionSerializer(serializers.ModelSerializer):
class Meta:
model=models.CourseSection
fields=['name','orders','duration','free_trail','section_link','section_type_name']
class CourseChapterSerializer(serializers.ModelSerializer):
# 子序列化的方式
coursesections=CourseSectionSerializer(many=True)
class Meta:
model=models.CourseChapter
fields=['name','summary','chapter','coursesections']
6.12 七牛雲視訊託管
# 1 註冊七牛雲,實名認證
https://www.qiniu.com/
# 2 登入--->管理控制檯--->物件儲存
# 3 新建空間---->上傳視訊--->拿到視訊連線
7 搜尋
6.13 後臺搜尋介面
# urls.py
router.register('search', views.CouresSearchView, 'search')
# views.py
class CouresSearchView(GenericViewSet,ListModelMixin):
queryset = models.Course.objects.filter(is_delete=False,is_show=True)
serializer_class = serializer.CourseModelSerializer
pagination_class = PageNumberPagination
filter_backends=[SearchFilter]
search_fields=['name']
7.2 Head元件更新
在Head.vue中增加搜尋框,js,css
# template
<form class="search">
<div class="tips" v-if="is_search_tip">
<span @click="search_action('Python')">Python</span>
<span @click="search_action('Linux')">Linux</span>
</div>
<input type="text" :placeholder="search_placeholder" @focus="on_search" @blur="off_search" v-model="search_word">
<button type="button" class="glyphicon glyphicon-search" @click="search_action(search_word)"></button>
</form>
</template>
# script標籤:
methods:{
search_action(search_word) {
if (!search_word) {
this.$message('請輸入要搜尋的內容');
return
}
//this.$route.params 從路徑中取值
//this.$route.query 從?後面的取
// if (search_word !== this.$route.query.word) {
// this.$router.push(`/search?word=${search_word}`);
// }
this.$router.push(`/search?word=${search_word}`);
this.search_word = '';
},
on_search() {
this.search_placeholder = '請輸入想搜尋的課程';
this.is_search_tip = false;
},
off_search() {
this.search_placeholder = '';
this.is_search_tip = true;
},
}
# css:
.search {
float: right;
position: relative;
margin-top: 22px;
margin-right: 10px;
}
.search input, .search button {
border: none;
outline: none;
background-color: white;
}
.search input {
border-bottom: 1px solid #eeeeee;
}
.search input:focus {
border-bottom-color: orange;
}
.search input:focus + button {
color: orange;
}
.search .tips {
position: absolute;
bottom: 3px;
left: 0;
}
.search .tips span {
border-radius: 11px;
background-color: #eee;
line-height: 22px;
display: inline-block;
padding: 0 7px;
margin-right: 3px;
cursor: pointer;
color: #aaa;
font-size: 14px;
}
.search .tips span:hover {
color: orange;
}
7.3 前臺搜尋頁面
# 1 vue攜帶引數給另一個頁面
-路由:path: '/free/detail/:pk',this.$route.params.pk 從路徑中取值
-search?word=dsa 從this.$route.query.word取出 從?後面的取
# 2 樣式
<template>
<div class="search-course course">
<Header/>
<h1>總共搜尋到 {{course_total}}課程</h1>
<!-- 課程列表 -->
<div class="main">
<div v-if="course_list.length > 0" class="course-list">
<div class="course-item" v-for="course in course_list" :key="course.name">
<div class="course-image">
<img :src="course.course_img" alt="">
</div>
<div class="course-info">
<h3>
<router-link :to="'/free/detail/'+course.id">{{course.name}}</router-link>
<span><img src="@/assets/img/avatar1.svg" alt="">{{course.students}}人已加入學習</span></h3>
<p class="teather-info">
{{course.teacher.name}} {{course.teacher.title}} {{course.teacher.signature}}
<span v-if="course.sections>course.pub_sections">共{{course.sections}}課時/已更新{{course.pub_sections}}課時</span>
<span v-else>共{{course.sections}}課時/更新完成</span>
</p>
<ul class="section-list">
<li v-for="(section, key) in course.section_list" :key="section.name"><span
class="section-title">0{{key+1}} | {{section.name}}</span>
<span class="free" v-if="section.free_trail">免費</span></li>
</ul>
<div class="pay-box">
<div v-if="course.discount_type">
<span class="discount-type">{{course.discount_type}}</span>
<span class="discount-price">¥{{course.real_price}}元</span>
<span class="original-price">原價:{{course.price}}元</span>
</div>
<span v-else class="discount-price">¥{{course.price}}元</span>
<span class="buy-now">立即購買</span>
</div>
</div>
</div>
</div>
<div v-else style="text-align: center; line-height: 60px">
沒有搜尋結果
</div>
<div class="course_pagination block">
<el-pagination
@size-change="handleSizeChange"
@current-change="handleCurrentChange"
:current-page.sync="filter.page"
:page-sizes="[2, 3, 5, 10]"
:page-size="filter.page_size"
layout="sizes, prev, pager, next"
:total="course_total">
</el-pagination>
</div>
</div>
</div>
</template>
<script>
import Header from '../components/Head'
export default {
name: "SearchCourse",
components: {
Header,
},
data() {
return {
course_list: [],
course_total: 0,
filter: {
page_size: 10,
page: 1,
search: '',
}
}
},
created() {
this.get_course()
},
watch: {
'$route.query' () {
this.get_course()
}
},
methods: {
handleSizeChange(val) {
// 每頁資料量發生變化時執行的方法
this.filter.page = 1;
this.filter.page_size = val;
},
handleCurrentChange(val) {
// 頁碼發生變化時執行的方法
this.filter.page = val;
},
get_course() {
// 獲取搜尋的關鍵字
this.filter.search = this.$route.query.word || this.$route.query.wd;
// 獲取課程列表資訊
this.$axios.get(`${this.$settings.base_url}/course/search/`, {
params: this.filter
}).then(response => {
// 如果後臺不分頁,資料在response.data中;如果後臺分頁,資料在response.data.results中
this.course_list = response.data.results;
this.course_total = response.data.count;
}).catch(() => {
this.$message({
message: "獲取課程資訊有誤,請聯絡客服工作人員"
})
})
}
}
}
</script>
<style scoped>
.course {
background: #f6f6f6;
}
.course .main {
width: 1100px;
margin: 35px auto 0;
}
.course .condition {
margin-bottom: 35px;
padding: 25px 30px 25px 20px;
background: #fff;
border-radius: 4px;
box-shadow: 0 2px 4px 0 #f0f0f0;
}
.course .cate-list {
border-bottom: 1px solid #333;
border-bottom-color: rgba(51, 51, 51, .05);
padding-bottom: 18px;
margin-bottom: 17px;
}
.course .cate-list::after {
content: "";
display: block;
clear: both;
}
.course .cate-list li {
float: left;
font-size: 16px;
padding: 6px 15px;
line-height: 16px;
margin-left: 14px;
position: relative;
transition: all .3s ease;
cursor: pointer;
color: #4a4a4a;
border: 1px solid transparent; /* transparent 透明 */
}
.course .cate-list .title {
color: #888;
margin-left: 0;
letter-spacing: .36px;
padding: 0;
line-height: 28px;
}
.course .cate-list .this {
color: #ffc210;
border: 1px solid #ffc210 !important;
border-radius: 30px;
}
.course .ordering::after {
content: "";
display: block;
clear: both;
}
.course .ordering ul {
float: left;
}
.course .ordering ul::after {
content: "";
display: block;
clear: both;
}
.course .ordering .condition-result {
float: right;
font-size: 14px;
color: #9b9b9b;
line-height: 28px;
}
.course .ordering ul li {
float: left;
padding: 6px 15px;
line-height: 16px;
margin-left: 14px;
position: relative;
transition: all .3s ease;
cursor: pointer;
color: #4a4a4a;
}
.course .ordering .title {
font-size: 16px;
color: #888;
letter-spacing: .36px;
margin-left: 0;
padding: 0;
line-height: 28px;
}
.course .ordering .this {
color: #ffc210;
}
.course .ordering .price {
position: relative;
}
.course .ordering .price::before,
.course .ordering .price::after {
cursor: pointer;
content: "";
display: block;
width: 0px;
height: 0px;
border: 5px solid transparent;
position: absolute;
right: 0;
}
.course .ordering .price::before {
border-bottom: 5px solid #aaa;
margin-bottom: 2px;
top: 2px;
}
.course .ordering .price::after {
border-top: 5px solid #aaa;
bottom: 2px;
}
.course .ordering .price_up::before {
border-bottom-color: #ffc210;
}
.course .ordering .price_down::after {
border-top-color: #ffc210;
}
.course .course-item:hover {
box-shadow: 4px 6px 16px rgba(0, 0, 0, .5);
}
.course .course-item {
width: 1100px;
background: #fff;
padding: 20px 30px 20px 20px;
margin-bottom: 35px;
border-radius: 2px;
cursor: pointer;
box-shadow: 2px 3px 16px rgba(0, 0, 0, .1);
/* css3.0 過渡動畫 hover 事件操作 */
transition: all .2s ease;
}
.course .course-item::after {
content: "";
display: block;
clear: both;
}
/* 頂級元素 父級元素 當前元素{} */
.course .course-item .course-image {
float: left;
width: 423px;
height: 210px;
margin-right: 30px;
}
.course .course-item .course-image img {
max-width: 100%;
max-height: 210px;
}
.course .course-item .course-info {
float: left;
width: 596px;
}
.course-item .course-info h3 a {
font-size: 26px;
color: #333;
font-weight: normal;
margin-bottom: 8px;
}
.course-item .course-info h3 span {
font-size: 14px;
color: #9b9b9b;
float: right;
margin-top: 14px;
}
.course-item .course-info h3 span img {
width: 11px;
height: auto;
margin-right: 7px;
}
.course-item .course-info .teather-info {
font-size: 14px;
color: #9b9b9b;
margin-bottom: 14px;
padding-bottom: 14px;
border-bottom: 1px solid #333;
border-bottom-color: rgba(51, 51, 51, .05);
}
.course-item .course-info .teather-info span {
float: right;
}
.course-item .section-list::after {
content: "";
display: block;
clear: both;
}
.course-item .section-list li {
float: left;
width: 44%;
font-size: 14px;
color: #666;
padding-left: 22px;
/* background: url("路徑") 是否平鋪 x軸位置 y軸位置 */
background: url("/src/assets/img/play-icon-gray.svg") no-repeat left 4px;
margin-bottom: 15px;
}
.course-item .section-list li .section-title {
/* 以下3句,文字內容過多,會自動隱藏,並顯示省略符號 */
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
display: inline-block;
max-width: 200px;
}
.course-item .section-list li:hover {
background-image: url("/src/assets/img/play-icon-yellow.svg");
color: #ffc210;
}
.course-item .section-list li .free {
width: 34px;
height: 20px;
color: #fd7b4d;
vertical-align: super;
margin-left: 10px;
border: 1px solid #fd7b4d;
border-radius: 2px;
text-align: center;
font-size: 13px;
white-space: nowrap;
}
.course-item .section-list li:hover .free {
color: #ffc210;
border-color: #ffc210;
}
.course-item {
position: relative;
}
.course-item .pay-box {
position: absolute;
bottom: 20px;
width: 600px;
}
.course-item .pay-box::after {
content: "";
display: block;
clear: both;
}
.course-item .pay-box .discount-type {
padding: 6px 10px;
font-size: 16px;
color: #fff;
text-align: center;
margin-right: 8px;
background: #fa6240;
border: 1px solid #fa6240;
border-radius: 10px 0 10px 0;
float: left;
}
.course-item .pay-box .discount-price {
font-size: 24px;
color: #fa6240;
float: left;
}
.course-item .pay-box .original-price {
text-decoration: line-through;
font-size: 14px;
color: #9b9b9b;
margin-left: 10px;
float: left;
margin-top: 10px;
}
.course-item .pay-box .buy-now {
width: 120px;
height: 38px;
background: transparent;
color: #fa6240;
font-size: 16px;
border: 1px solid #fd7b4d;
border-radius: 3px;
transition: all .2s ease-in-out;
float: right;
text-align: center;
line-height: 38px;
position: absolute;
right: 0;
bottom: 5px;
}
.course-item .pay-box .buy-now:hover {
color: #fff;
background: #ffc210;
border: 1px solid #ffc210;
}
.course .course_pagination {
margin-bottom: 60px;
text-align: center;
}
</style>
8 支付寶
8.1 支付寶sdk使用方法
# 沙箱環境:https://openhome.alipay.com/platform/appDaily.htm
# https://opendocs.alipay.com/open/291/106097/ 下載軟體,生成祕鑰(公鑰私鑰)
# 取出公鑰,配置到支付寶平臺,會生成一個支付寶公鑰
# 安裝支付寶sdk
# pip install python-alipay-sdk
from alipay import AliPay
# app_private_key_string = open("/path/to/your/private/key.pem").read()
# alipay_public_key_string = open("/path/to/alipay/public/key.pem").read()
app_private_key_string = """-----BEGIN RSA PRIVATE KEY-----
MIIEpAIBAAKCAQEApkFnp38PSslhn4wvRarL40+WtVpan6tVT55FWzAzLfQ8PfqZE2qW7mCJy1/zM07+S5L+X0pC+
...
-----END RSA PRIVATE KEY-----
"""
alipay_public_key_string = """-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwU9mMZHHlQPE9FcxVtXOXhbWCtuDZLJRVCiofdbTVmRXr...
-----END PUBLIC KEY-----
"""
alipay = AliPay(
appid="2016092000554611", # 支付寶appid
app_notify_url='http://127.0.0.1:8000/home/', # the default notify path
app_private_key_string=app_private_key_string,
alipay_public_key_string=alipay_public_key_string,
sign_type="RSA2", # RSA or RSA2
debug=True # False by default
)
alipay_url='https://openapi.alipaydev.com/gateway.do?'
order_string = alipay.api_alipay_trade_page_pay (
out_trade_no="20161112www4334",
total_amount=9999,
subject='韓紅版充氣娃娃',
return_url="https://www.luffycity.com/free-course",
notify_url="https://www.luffycity.com/free-course"
)
print(alipay_url+order_string)
8.2 支付寶功能二次封裝
al_pay
-pem
-__init__.py
-pay.py
-setting.py
#__init__.py
from .pay import alipay,gateway
#pay.py
from alipay import AliPay
from . import setting
alipay = AliPay(
appid=setting.APPID,
app_notify_url=None, # the default notify path
app_private_key_string=setting.APP_PRIVATE_KEY_STRING,
# alipay public key, do not use your own public key!
alipay_public_key_string=setting.ALIPAY_PUBLIC_KEY_STRING,
sign_type=setting.SIGN_TYPE, # RSA or RSA2
debug=setting.DEBUG # False by default
)
gateway=setting.GATEWAY
# setting.py
import os
APPID="2016092000554611"
APP_PRIVATE_KEY_STRING = open(os.path.join(os.path.dirname(__file__),'pem','private_key.pem')).read()
ALIPAY_PUBLIC_KEY_STRING = open(os.path.join(os.path.dirname(__file__),'pem','al_public_key.pem')).read()
SIGN_TYPE='RSA2'
DEBUG=True
GATEWAY='https://openapi.alipaydev.com/gateway.do?' if DEBUG else 'https://openapi.alipay.com/gateway.do?'
9 訂單
9.1 訂單模型與表分析
# 訂單表分析
-訂單表
-訂單詳情
from django.db import models
from django.db import models
from user.models import User
from course.models import Course
# 訂單表
class Order(models.Model):
"""訂單模型"""
status_choices = (
(0, '未支付'),
(1, '已支付'),
(2, '已取消'),
(3, '超時取消'),
)
pay_choices = (
(1, '支付寶'),
(2, '微信支付'),
)
subject = models.CharField(max_length=150, verbose_name="訂單標題")
total_amount = models.DecimalField(max_digits=10, decimal_places=2, verbose_name="訂單總價", default=0)
out_trade_no = models.CharField(max_length=64, verbose_name="訂單號", unique=True)
trade_no = models.CharField(max_length=64, null=True, verbose_name="流水號") # 支付寶生成回來的
order_status = models.SmallIntegerField(choices=status_choices, default=0, verbose_name="訂單狀態")
pay_type = models.SmallIntegerField(choices=pay_choices, default=1, verbose_name="支付方式")
pay_time = models.DateTimeField(null=True, verbose_name="支付時間")
# 一個使用者可以下多個訂單,一個訂單隻屬於一個使用者,一對多的關係,關聯欄位寫在多個一方,寫在order方
user = models.ForeignKey(User, related_name='order_user', on_delete=models.DO_NOTHING, db_constraint=False, verbose_name="下單使用者")
created_time = models.DateTimeField(auto_now_add=True, verbose_name='建立時間')
updated_time = models.DateTimeField(auto_now=True, verbose_name='最後更新時間')
class Meta:
db_table = "luffy_order"
verbose_name = "訂單記錄"
verbose_name_plural = "訂單記錄"
def __str__(self):
return "%s - ¥%s" % (self.subject, self.total_amount)
@property
def courses(self):
data_list = []
for item in self.order_courses.all():
data_list.append({
"id": item.id,
"course_name": item.course.name,
"real_price": item.real_price,
})
return data_list
# 訂單詳情表
# 訂單和詳情是一對多,關聯欄位寫在多個的一方,寫在訂單詳情表中
class OrderDetail(models.Model):
"""訂單詳情"""
order = models.ForeignKey(Order, related_name='order_courses', on_delete=models.CASCADE, db_constraint=False, verbose_name="訂單")
course = models.ForeignKey(Course, related_name='course_orders', on_delete=models.SET_NULL, db_constraint=False, verbose_name="課程",null=True)
price = models.DecimalField(max_digits=6, decimal_places=2, verbose_name="課程原價")
real_price = models.DecimalField(max_digits=6, decimal_places=2, verbose_name="課程實價")
class Meta:
db_table = "luffy_order_detail"
verbose_name = "訂單詳情"
verbose_name_plural = "訂單詳情"
def __str__(self):
try:
return "%s的訂單:%s" % (self.course.name, self.order.out_trade_no)
except:
return super().__str__()
9.2 訂單模組介面分析
# 1 支付介面(生成訂單,生成支付連線,返回支付連線)
-order表和orderdetail表插入資料,重寫create方法
-生成訂單號(uuid)
-登入後才能生成訂單(jwt認證)
-當前登入使用者就是下單使用者,存到order表中
-下了三個課程,總價格100,前端提交的價格是99,價格不符需要進行異常處理
# 1)訂單總價校驗
# 2)生成訂單號
# 3)支付使用者:request.user
# 4)支付連結生成
# 5)入庫(兩個表)的資訊準備
# 2 支付寶非同步回撥的post介面(驗證簽名,修改訂單狀態)
# 3 當支付寶get回撥前端,vue元件一建立,立馬向後端發一個請求(get)
9.3 支付介面
9.3.1 支付介面思路
# 1 前端傳輸的資料格式
{course:[1,2,3],total_amount:100,subject:xx商品,pay_type:1,}
# 2 後端接到資料要校驗
course:[1,2,3]===>course:[obj1,obj2,obj3]
可以在序列化類中定義:
course = serializers.PrimaryKeyRelatedField(queryset=Course.objects.all(), write_only=True, many=True)
# 3 需要在序列化類的validate中處理的邏輯
# 1)訂單總價校驗:
-取出一個個課程的價格,累加是否等於傳入的總價格
# 2)生成訂單號
-通過uuid生成
# 3)支付使用者:request.user
-通過檢視和序列化類之間的橋樑context物件傳遞
-重新檢視類中的create方法,把request物件放入context
-self.context.get('request').user
# 4)支付連結生成
-匯入封裝的支付寶支付,生成
order_string = alipay.api_alipay_trade_page_pay (
out_trade_no=out_trade_no,
total_amount=total_amout,
subject=subject,
return_url=settings.RETURN_URL, # get回撥,前臺地址
notify_url=settings.NOTIFY_URL # post回撥,後臺地址
)
# 5)入庫(兩個表)的資訊準備
-把user放入attrs中
-把訂單號,放入attrs中
attrs['user']=user
attrs['out_trade_no']=out_trade_no
self.context['pay_url']=pay_url
# 4 重寫序列化類的create方法
-把course_list彈出來
-order=models.Order.objects.create(**validated_data)
9.3.2 支付介面程式碼
# urls.py
router = SimpleRouter()
router.register('pay', views.PayView, 'pay')
urlpatterns = [
path('', include(router.urls)),
]
# views.py
class PayView(GenericViewSet,CreateModelMixin):
authentication_classes = [JSONWebTokenAuthentication,]
permission_classes = [IsAuthenticated,]
queryset = models.Order.objects.all()
serializer_class = serializer.OrderSerializer
# 重寫create方法
def create(self, request, *args, **kwargs):
serializer = self.get_serializer(data=request.data,context={'request':request})
serializer.is_valid(raise_exception=True)
self.perform_create(serializer)
return Response(serializer.context.get('pay_url'))
# serializer.py
class OrderSerializer(serializers.ModelSerializer):
# 前端傳什麼資料過來{course:[1,2,3],total_amount:100,subject:xx商品,pay_type:1,}
# user欄位需要,但是不是傳的,使用了jwt
# 需要把course:[1,2,3] 處理成 course:[obj1,obj2,obj3]
# 課時:[1,4,6,]===>課時:[obj1,obj4,obj6,]
# course=serializers.CharField()
course=serializers.PrimaryKeyRelatedField(queryset=Course.objects.all(), write_only=True, many=True)
class Meta:
model = models.Order
fields = ['total_amount','subject','pay_type','course']
extra_kwargs={
'total_amount':{'required':True},
'pay_type': {'required': True},
}
def _check_price(self,attrs):
total_amount=attrs.get('total_amount')
course_list=attrs.get('course')
total_price=0
for course in course_list:
total_price+=course.price
if total_price!=total_amount:
raise ValidationError('價格不合法')
return total_amount
def _gen_out_trade_no(self):
import uuid
return str(uuid.uuid4()).replace('-','')
def _get_user(self):
# 需要request物件(需要檢視通過context把reuqest物件傳入。重寫create方法)
request=self.context.get('request')
return request.user
def _gen_pay_url(self,out_trade_no,total_amout,subject):
# total_amout是Decimal型別,識別不了,需要轉換成float型別
from luffyapi.libs.al_pay import alipay,gateway
order_string = alipay.api_alipay_trade_page_pay (
out_trade_no=out_trade_no,
total_amount=float(total_amout),
subject=subject,
return_url=settings.RETURN_URL, # get回撥,前臺地址
notify_url=settings.NOTIFY_URL # post回撥,後臺地址
)
return gateway+order_string
def _before_create(self,attrs,user,pay_url,out_trade_no):
attrs['user']=user
attrs['out_trade_no']=out_trade_no
self.context['pay_url']=pay_url
def validate(self, attrs):
'''
# 1)訂單總價校驗
# 2)生成訂單號
# 3)支付使用者:request.user
# 4)支付連結生成
# 5)入庫(兩個表)的資訊準備
'''
# 1)訂單總價校驗
total_amout = self._check_price(attrs)
# 2)生成訂單號
out_trade_no=self._gen_out_trade_no()
# 3)支付使用者:request.user
user=self._get_user()
# 4)支付連結生成
pay_url=self._gen_pay_url(out_trade_no,total_amout,attrs.get('subject'))
# 5)入庫(兩個表)的資訊準備
self._before_create(attrs,user,pay_url,out_trade_no)
return attrs
def create(self, validated_data):
course_list=validated_data.pop('course')
order=models.Order.objects.create(**validated_data)
for course in course_list:
models.OrderDetail.objects.create(order=order,course=course,price=course.price,real_price=course.price)
return order
9.4 支付回撥介面配置
# dev.py
# 上線後必須換成公網地址
# 後臺基URL
BASE_URL = 'http://127.0.0.1:8000'
# 前臺基URL
LUFFY_URL = 'http://127.0.0.1:8080'
# 支付寶同步非同步回撥介面配置
# 後臺非同步回撥介面
NOTIFY_URL = BASE_URL + "/order/success/"
# 前臺同步回撥介面,沒有 / 結尾
RETURN_URL = LUFFY_URL + "/pay/success"
9.5 前臺生成訂單並跳轉
# FreeCourse.vue
<span class="buy-now" @click="buy_now(course)">立即購買</span>
# script
buy_now(course) {
let token = this.$cookies.get('token')
if (!token) {
this.$message({
message: "您還沒有登入,請先登入",
})
return false
}
this.$axios({
method: 'post',
url: this.$settings.base_url + '/order/pay/',
data: {
"total_amount": course.price,
"subject": course.name,
"pay_type": 1,
"course": [
course.id,
]
},
headers: {Authorization: 'jwt ' + token}
}
).then(response => {
console.log(response.data)
let pay_url=response.data
//前端傳送get請求
open(pay_url,'_self')
}).catch(error => {
})
},
9.6 前臺支付成功頁面
# 路由
{
path: '/pay/success',
name: 'PaySuccess',
component: PaySuccess
},
#PaySuccess.vue
<template>
<div class="pay-success">
<!--如果是單獨的頁面,就沒必要展示導航欄(帶有登入的使用者)-->
<Header/>
<div class="main">
<div class="title">
<div class="success-tips">
<p class="tips">您已成功購買 1 門課程!</p>
</div>
</div>
<div class="order-info">
<p class="info"><b>訂單號:</b><span>{{ result.out_trade_no }}</span></p>
<p class="info"><b>交易號:</b><span>{{ result.trade_no }}</span></p>
<p class="info"><b>付款時間:</b><span><span>{{ result.timestamp }}</span></span></p>
</div>
<div class="study">
<span>立即學習</span>
</div>
</div>
</div>
</template>
<script>
import Header from "@/components/Head"
export default {
name: "Success",
data() {
return {
result: {},
};
},
created() {
// url後拼接的引數:?及後面的所有引數 => ?a=1&b=2
// console.log(location.search);
// 解析支付寶回撥的url引數
let params = location.search.substring(1); // 去除? => a=1&b=2
let items = params.length ? params.split('&') : []; // ['a=1', 'b=2']
//逐個將每一項新增到args物件中
for (let i = 0; i < items.length; i++) { // 第一次迴圈a=1,第二次b=2
let k_v = items[i].split('='); // ['a', '1']
//解碼操作,因為查詢字串經過編碼的
if (k_v.length >= 2) {
// url編碼反解
let k = decodeURIComponent(k_v[0]);
this.result[k] = decodeURIComponent(k_v[1]);
// 沒有url編碼反解
// this.result[k_v[0]] = k_v[1];
}
}
// 解析後的結果
// console.log(this.result);
// 把位址列上面的支付結果,再get請求轉發給後端
this.$axios({
url: this.$settings.base_url + '/order/success/' + location.search,
method: 'get',
}).then(response => {
console.log(response.data);
}).catch(() => {
console.log('支付結果同步失敗');
})
},
components: {
Header,
}
}
</script>
<style scoped>
.main {
padding: 60px 0;
margin: 0 auto;
width: 1200px;
background: #fff;
}
.main .title {
display: flex;
-ms-flex-align: center;
align-items: center;
padding: 25px 40px;
border-bottom: 1px solid #f2f2f2;
}
.main .title .success-tips {
box-sizing: border-box;
}
.title img {
vertical-align: middle;
width: 60px;
height: 60px;
margin-right: 40px;
}
.title .success-tips {
box-sizing: border-box;
}
.title .tips {
font-size: 26px;
color: #000;
}
.info span {
color: #ec6730;
}
.order-info {
padding: 25px 48px;
padding-bottom: 15px;
border-bottom: 1px solid #f2f2f2;
}
.order-info p {
display: -ms-flexbox;
display: flex;
margin-bottom: 10px;
font-size: 16px;
}
.order-info p b {
font-weight: 400;
color: #9d9d9d;
white-space: nowrap;
}
.study {
padding: 25px 40px;
}
.study span {
display: block;
width: 140px;
height: 42px;
text-align: center;
line-height: 42px;
cursor: pointer;
background: #ffc210;
border-radius: 6px;
font-size: 16px;
color: #fff;
}
</style>
9.7 支付寶get回撥的引數
charset=utf-8&
out_trade_no=17fcf8cac17c442bbbc114df6004afff&
method=alipay.trade.page.pay.return&
total_amount=99.00&
sign=WfK2beWFKvVaTHXpREi8HqZtFRH3JbeIvkliReYvfuhAsqaxHguARKtW6jUqUdZinm7ZSaYE1NrBRQa3%2BLquMk6uMnxE0i%2FTXIu4%2FmNTCEqSUlG8fTRPwC2%2BuU4nN1Ym0eM4puzAc2TUnEJnXCGKP9UxMifN3cjqR5BP%2B3RRngZSS4IQeogjurpfdiIolLzed%2FHTWbc4HqvWlWn9JuLmFGTtKHvRRKFr1hqq8Pj%2Fe3Al8kieDN9Q7JhEdC6F5ROo9rLlmUJtevkjI22oRScrfJl5hb%2BeYosxNg3WktmYKlF5vsKeZKKnLayAvKGoySLvaWk90x0LijHzzf2%2F8a9s3w%3D%3D&
trade_no=2020072922001480160500851368&
auth_app_id=2016092000554611&
version=1.0&
app_id=2016092000554611&
sign_type=RSA2&
seller_id=2088102176466324&
timestamp=2020-07-29%2015%3A06%3A44
9.8 後端同步非同步支付回撥介面
class SuccessView(APIView):
def get(self,request,*args,**kwargs):
out_trade_no=request.query_params.get('out_trade_no')
order=models.Order.objects.filter(out_trade_no=out_trade_no).first()
if order.order_status==1:
return Response(True)
else:
return Response(False)
def post(self,request,*args,**kwargs):
'''
支付寶回撥介面
'''
from luffyapi.libs.al_pay import alipay
from luffyapi.utils.logger import log
data = request.data
out_trade_no=data.get('out_trade_no',None)
gmt_payment=data.get('gmt_payment',None)
signature = data.pop("sign")
# 驗證簽名
success = alipay.verify(data, signature)
if success and data["trade_status"] in ("TRADE_SUCCESS", "TRADE_FINISHED"):
models.Order.objects.filter(out_trade_no=out_trade_no).update(order_status=1,pay_time=gmt_payment)
log.info('%s訂單支付成功'%out_trade_no)
return Response('success')
else:
log.info('%s訂單有問題' % out_trade_no)
return Response('error')
10 上線
詳見專案上線篇
上線前準備:
# 前端執行 npm run build 把你寫的vue程式碼編譯成html,css,js
# 後端程式碼
-修改setting中的pro.py
-專案根路徑新建一個manage_pro.py(把原來的manage.py複製改動上線的配置檔案)
-wsgi.py:改成線上的dev