目錄
1.導航欄的實現
1.設計導航欄的model模型類
apps/home/models.py
class Nav(BaseModel): """導航選單模型""" POSITION_OPTION = ( (1, "頂部導航"), (2, "腳部導航"), ) title = models.CharField(max_length=500, verbose_name="導航標題") link = models.CharField(max_length=500, verbose_name="導航連結") position = models.IntegerField(choices=POSITION_OPTION, default=1, verbose_name="導航位置") is_site = models.BooleanField(default=False, verbose_name="是否是站外地址") class Meta: db_table = 'ly_nav' verbose_name = '導航選單' verbose_name_plural = verbose_name # 自定義方法[自定義欄位或者自定義工具方法] def __str__(self): return self.title
2.在Xadmin中註冊導航欄模型類
apps/home/adminx.py
# 導航選單 class NavModelAdmin(object): list_display=["title","link","is_show","is_site","position"] xadmin.site.register(models.Nav, NavModelAdmin)
執行資料庫遷移同步指令
python manage.py makemigrations
python manage.py migrate
3.在Xadmin中新增一些導航欄資料
4.註冊導航欄的URL
from django.urls import path,re_path from . import views urlpatterns = [ ...... path("nav/header/", views.HeaderNavListAPIView.as_view()), ]
5.新建導航欄的檢視類
from .models import Nav from .serializers import NavModelSerializer class HeaderNavListAPIView(ListAPIView): """頂部導航選單檢視""" queryset = Nav.objects.filter(is_show=True, is_deleted=False,position=1).order_by("-orders","-id")[:constants.HEADER_NAV_LENGTH] serializer_class = NavModelSerializer ''' position=1代表是頂部導航,position=2代表是底部導航 '''
6.新建導航欄的序列化器
from .models import Nav class NavModelSerializer(serializers.ModelSerializer): """導航選單序列化器""" class Meta: model = Nav fields = ["id","title","link","is_site"]
7.除錯一下看是否能獲取到資料
8.編寫導航欄vue元件的程式碼
1.Vheader元件載入時,通過axios的get請求去請求後端的資料
<script> export default { name: "Header", data(){ return { ...... nav_data_list:[], } }, methods:{ ...... get_nav_data(){ this.$axios.get(`${this.$settings.Host}/home/nav/header/`,).then((res)=>{ this.nav_data_list = res.data; }) .catch((error)=>{ console.log(error); }) } }, // 注意:一定要寫created方法來觸發函式 created() { this.get_nav_data(); } } </script>
2.後端資料已經拿到,接下來要在前端展示出來
思路:for迴圈取出導航欄所有的資料,判斷導航欄的標題是站內跳轉還是站外跳轉
如果是站內跳轉就用router-link,如果是站外跳轉就用a href
value就是你從後端獲取到的資料 value.link value.is_site value.title就可以取到相應的值
<el-col class="nav" :span="10"> <el-row> <el-col :span="3" v-for="(value,index) in nav_data_list" :key="index"> <a :href="value.link" class="active" v-if="value.is_site">{{value.title}}</a> <router-link :to="value.link" v-else>{{value.title}}</router-link> </el-col> </el-row> </el-col>
2.登入前戲:使用者表初始化
1.Login.vue
<template> <div class="login box"> <img src="../../static/img/Loginbg.3377d0c.jpg" alt=""> <div class="login"> <div class="login-title"> <img src="../../static/img/Logotitle.1ba5466.png" alt=""> <p>幫助有志向的年輕人通過努力學習獲得體面的工作和生活!</p> </div> <div class="login_box"> <div class="title"> <span @click="login_type=0">密碼登入</span> <span @click="login_type=1">簡訊登入</span> </div> <div class="inp" v-if="login_type==0"> <input v-model = "username" type="text" placeholder="使用者名稱 / 手機號碼" class="user"> <input v-model = "password" type="password" name="" class="pwd" placeholder="密碼"> <div id="geetest1"></div> <div class="rember"> <p> <input type="checkbox" class="no" name="a" v-model="remember"/> <span>記住密碼</span> </p> <p>忘記密碼</p> </div> <button class="login_btn" @click="loginHandle">登入</button> <p class="go_login" >沒有賬號 <span>立即註冊</span></p> </div> <div class="inp" v-show="login_type==1"> <input v-model = "username" type="text" placeholder="手機號碼" class="user"> <input v-model = "password" type="text" class="pwd" placeholder="簡訊驗證碼"> <button id="get_code">獲取驗證碼</button> <button class="login_btn">登入</button> <p class="go_login" >沒有賬號 <span>立即註冊</span></p> </div> </div> </div> </div> </template> <script> export default { name: 'Login', data(){ return { login_type: 0, username:"", password:"", remember:false, } }, methods:{ loginHandle(){ this.$axios.post(`${this.$settings.Host}/users/login/`,{ username:this.username, password:this.password, }).then((res)=>{ console.log(res); // console.log(this.remember); if (this.remember){ localStorage.token = res.data.token; localStorage.username = res.data.username; localStorage.id = res.data.id; sessionStorage.removeItem('token'); sessionStorage.removeItem('username'); sessionStorage.removeItem('id'); }else { sessionStorage.token = res.data.token; sessionStorage.username = res.data.username; sessionStorage.id = res.data.id; localStorage.removeItem('token'); localStorage.removeItem('username'); localStorage.removeItem('id'); } this.$router.push('/'); }).catch((error)=>{ this.$alert('使用者名稱password error', 'error msg', { confirmButtonText: '確定', }); }) } }, }; </script> <style scoped> .box{ width: 100%; height: 100%; position: relative; overflow: hidden; } .box img{ width: 100%; min-height: 100%; } .box .login { position: absolute; width: 500px; height: 400px; top: 0; left: 0; margin: auto; right: 0; bottom: 0; top: -338px; } .login .login-title{ width: 100%; text-align: center; } .login-title img{ width: 190px; height: auto; } .login-title p{ font-family: PingFangSC-Regular; font-size: 18px; color: #fff; letter-spacing: .29px; padding-top: 10px; padding-bottom: 50px; } .login_box{ width: 400px; height: auto; background: #fff; box-shadow: 0 2px 4px 0 rgba(0,0,0,.5); border-radius: 4px; margin: 0 auto; padding-bottom: 40px; } .login_box .title{ font-size: 20px; color: #9b9b9b; letter-spacing: .32px; border-bottom: 1px solid #e6e6e6; display: flex; justify-content: space-around; padding: 50px 60px 0 60px; margin-bottom: 20px; cursor: pointer; } .login_box .title span:nth-of-type(1){ color: #4a4a4a; border-bottom: 2px solid #84cc39; } .inp{ width: 350px; margin: 0 auto; } .inp input{ border: 0; outline: 0; width: 100%; height: 45px; border-radius: 4px; border: 1px solid #d9d9d9; text-indent: 20px; font-size: 14px; background: #fff !important; } .inp input.user{ margin-bottom: 16px; } .inp .rember{ display: flex; justify-content: space-between; align-items: center; position: relative; margin-top: 10px; } .inp .rember p:first-of-type{ font-size: 12px; color: #4a4a4a; letter-spacing: .19px; margin-left: 22px; display: -ms-flexbox; display: flex; -ms-flex-align: center; align-items: center; /*position: relative;*/ } .inp .rember p:nth-of-type(2){ font-size: 14px; color: #9b9b9b; letter-spacing: .19px; cursor: pointer; } .inp .rember input{ outline: 0; width: 30px; height: 45px; border-radius: 4px; border: 1px solid #d9d9d9; text-indent: 20px; font-size: 14px; background: #fff !important; } .inp .rember p span{ display: inline-block; font-size: 12px; width: 100px; /*position: absolute;*/ /*left: 20px;*/ } #geetest{ margin-top: 20px; } .login_btn{ width: 100%; height: 45px; background: #84cc39; border-radius: 5px; font-size: 16px; color: #fff; letter-spacing: .26px; margin-top: 30px; } .inp .go_login{ text-align: center; font-size: 14px; color: #9b9b9b; letter-spacing: .26px; padding-top: 20px; } .inp .go_login span{ color: #84cc39; cursor: pointer; } </style>
2.在index.js中新增Login元件的路由
import Vue from 'vue' import Router from 'vue-router' import Home from '@/components/Home' import Login from '@/components/Login' Vue.use(Router) export default new Router({ mode:'history', routes: [ ...... { path:'/user/login', component: Login } ] })
3.將導航欄Vheader元件的登入按鈕設定一個跳轉連線
<!-- 點選登入按鈕 跳轉到登入的頁面:/user/login --> <router-link to="user/login"><button class="signin">登入</button></router-link>
4.將導航欄Vheader元件中的登入狀態token改為false
將token值改為false,讓首頁是未登入狀態,這樣才能顯示登入註冊的按鈕
<script> export default { name: "Header", data(){ return { token:false,
}
5.建立users應用
1.建立user應用
python manage.py startapp users
2.在dev.py配置檔案的INSTALL_APPS加上users
3.為user應用配置總路由
path('users/',include("users.urls")),
6.建立user模型類
在django的auth模組是自帶一個使用者表的,我們寫使用者表時可以繼承django自帶的使用者表.
from django.db import models from django.contrib.auth.models import AbstractUser # AbstractUser是django自帶的一個使用者表 class User(AbstractUser): phone = models.CharField(max_length=16, null=True, blank=True) wechat = models.CharField(max_length=16, null=True, blank=True) class Meta: db_table = 'ly_user' verbose_name = '使用者表' verbose_name_plural = verbose_name
1.使用django自帶的Abstractuser表加自己的擴充套件欄位作為專案的使用者表【已經完成】
2.後臺登入Xadmin的後臺管理系統的那些使用者不再使用原來django自帶的使用者表,而是使用自己建立的使用者表
Xadmin使用自己建立的使用者表
需要在setting中配置一下,讓django別再使用自帶的那個user表,而是使用我們自己編寫的user表
dev.py
AUTH_USER_MODEL = 'users.User'
上面的操作應該建立在第一次資料庫遷移之前。
但是我們之前已經進行過幾次資料庫遷移了。所以需要做以下幾步操作:
// 0. 先把現有的資料庫匯出備份,然後清掉資料庫中所有的資料表。 // 1. 把開發者建立的所有子應用下面的migrations目錄下除了__init__.py以外的所有遷移檔案,只要涉及到使用者的,一律刪除,並將django-migrations表中的資料全部刪除。 // 2. 把django.contrib.admin.migrations目錄下除了__init__.py以外的所有遷移檔案,全部刪除。 // 3. 把django.contrib.auth.migrations目錄下除了__init__.py以外的所有遷移檔案,全部刪除。 // 4. 把reversion.migrations目錄下除了__init__.py以外的所有遷移檔案,全部刪除。這個不在django目錄裡面,在site-packages裡面,是xadmin安裝的時候帶的,它會記錄使用者資訊,也需要刪除 // 5. 把xadmin.migrations目錄下除了__init__.py以外的所有遷移檔案,全部刪除。 // 6. 刪除我們當前資料庫中的所有表 // 7. 接下來,執行資料遷移(makemigrations和migrate),回顧第0步中的資料,將資料匯入就可以了,以後如果要修改使用者相關資料,不需要重複本次操作,直接資料遷移即可。//
這些完成之後,建立一個超級使用者
python3 manage.py createsuperuser
會發現xadmin超級使用者的使用者名稱和密碼存到了自己的ly_user表中。我們達到了目的。
3.DjangoRestFramework JWT
1.jwt的安裝
pip install djangorestframework-jwt -i https://mirrors.aliyun.com/pypi/simple/
2.jwt的配置
dev.py
REST_FRAMEWORK = { 'DEFAULT_AUTHENTICATION_CLASSES': ( 'rest_framework_jwt.authentication.JSONWebTokenAuthentication', 'rest_framework.authentication.SessionAuthentication', 'rest_framework.authentication.BasicAuthentication', ), } import datetime JWT_AUTH = { 'JWT_EXPIRATION_DELTA': datetime.timedelta(days=1), }
注:JWT_EXPIRATION_DELTA 指明token的有效期
3.為jwt配置路由
apps/users/urls.py
為jwt設定路由 ,用來前端訪問url 獲取jwt token值
from rest_framework_jwt.views import obtain_jwt_token from django.urls import path urlpatterns = [ path(r'login/', obtain_jwt_token), ]
訪問 www.lyapi.com:8001/users/login 新增使用者名稱和密碼欄位 post提交後可得token值,如圖所示
4.前端通過axios請求獲取後端傳過來的token
後端返回給前端一個token值,前端接收token值並儲存起來
1.獲取使用者在前端輸入的使用者名稱和密碼
methods:{ loginHandle(){ this.$axios.post(`${this.$settings.Host}/users/login/`,{ username:this.username, password:this.password,
2.為login.vue的登入按鈕繫結一個LoginHandle事件
<button class="login_btn" @click="loginHandle">登入</button>
這個時候我們訪問www.lycity.com/user/login 輸入使用者名稱和密碼
檢視console 可以看到data中已經存放著token值 ==>這個時候前端已經拿到token值
3.引入session storage 和 local storage===>實現前端對token的儲存
session storage和local storage的區別
session storage 是臨時儲存 關閉瀏覽器就沒有了
local storage 是永久儲存
5.前端儲存token值
登入頁面有一個記住密碼的選項,我們就可以使用session storage 和 loca storage 來做一些事情
1.將記住密碼checkbox設定為v-model
當使用者勾選/沒有勾選 記住密碼 這個選項時,remember的值發生改變
Login.vue
<p> <input type="checkbox" class="no" name="a" v-model="remember"/> <span>記住密碼</span> </p>
2.我們先讓remember的值預設為false
export default { name: 'Login', data(){ return { ... remember:false, } },
所以根據 remember的值 就可以做if判斷了
6.擴充套件預設的返回值
方便在客戶端頁面中顯示當前登入使用者。
所以需要擴充套件一下
通過修改該檢視的返回值可以完成我們的需求
user/utils.py
def jwt_response_payload_handler(token, user=None, request=None): """ 自定義jwt認證成功返回資料 """ return { 'token': token, 'id': user.id, 'username': user.username }
除了擴充套件一下這個,還要修改一下下面的配置
settings/dev.py
# JWT JWT_AUTH = { 'JWT_EXPIRATION_DELTA': datetime.timedelta(days=1), 'JWT_RESPONSE_PAYLOAD_HANDLER': 'users.utils.jwt_response_payload_handler', }
這個配置的作用是:告訴jwt 響應結果的時候,按照我指定的 jwt_response_payload_handler這個方法來返回
jwt 認證成功返回了三個欄位 token id username 響應給前端
前端也應該接収這幾個欄位
methods:{ loginHandle(){ this.$axios.post(`${this.$settings.Host}/users/login/`,{ username:this.username, password:this.password, }).then((res)=>{ console.log(res); if (this.remember){ localStorage.token = res.data.token; localStorage.username = res.data.username; localStorage.id = res.data.id; ... }else { sessionStorage.token = res.data.token; sessionStorage.username = res.data.username; sessionStorage.id = res.data.id; ... }
final
''' 實現思路: 如果使用者標選了記住密碼 就將token值儲存在local storage中 如果使用者沒有標記 記住密碼 就將token值儲存在session storage中 ''' methods:{ loginHandle(){ this.$axios.post(`${this.$settings.Host}/users/login/`,{ username:this.username, password:this.password, }).then((res)=>{ console.log(res); // console.log(this.remember); if (this.remember){ localStorage.token = res.data.token; localStorage.username = res.data.username; localStorage.id = res.data.id; sessionStorage.removeItem('token'); sessionStorage.removeItem('username'); sessionStorage.removeItem('id'); }else { sessionStorage.token = res.data.token; sessionStorage.username = res.data.username; sessionStorage.id = res.data.id; localStorage.removeItem('token'); localStorage.removeItem('username'); localStorage.removeItem('id'); } this.$router.push('/');
Tip:關於session storage和local storage的使用方法
sessionStorage.變數名 = 變數值 // 儲存資料 sessionStorage.setItem("變數名","變數值") // 儲存資料 sessionStorage.變數名 // 讀取資料 sessionStorage.getItem("變數名") // 讀取資料 sessionStorage.removeItem("變數名") // 清除單個資料 sessionStorage.clear() // 清除所有sessionStorage儲存的資料 localStorage.變數名 = 變數值 // 儲存資料 localStorage.setItem("變數名","變數值") // 儲存資料 localStorage.變數名 // 讀取資料 localStorage.getItem("變數名") // 讀取資料 localStorage.removeItem("變數名") // 清除單個資料 localStorage.clear() // 清除所有sessionStorage儲存的資料
4.多條件登入
我們可以通過修改Django認證系統的認證後端(主要是authenticate方法)來支援登入賬號既可以是使用者名稱也可以是手機號。
官方說:修改Django認證系統的認證後端需要繼承django.contrib.auth.backends.ModelBackend,並重寫authenticate方法。
authenticate(self, request, username=None, password=None, **kwargs)
方法的引數說明:
-
request 本次認證的請求物件
-
username 本次認證提供的使用者賬號
-
password 本次認證提供的密碼
我們想要讓使用者既可以以使用者名稱登入,也可以以手機號登入,那麼對於authenticate方法而言,username引數即表示使用者名稱或者手機號。
重寫authenticate方法的思路:
-
根據username引數查詢使用者User物件,username引數可能是使用者名稱,也可能是手機號
-
若查詢到User物件,呼叫User物件的check_password方法檢查密碼是否正確
users/utils.py
def get_user_by_account(account): """ 根據帳號獲取user物件 :param account: 賬號,可以是使用者名稱,也可以是手機號 :return: User物件 或者 None """ try: # 查詢使用者名稱是否存在 user = models.User.objects.filter(Q(username=account)|Q(mobile=account)).first() except models.User.DoesNotExist: return None else: return user # 存在返回使用者名稱 from . import models from django.db.models import Q from django.contrib.auth.backends import ModelBackend class UsernameMobileAuthBackend(ModelBackend): """ 自定義使用者名稱或手機號認證 """ def authenticate(self, request, username=None, password=None, **kwargs): user = get_user_by_account(username) #if user is not None and user.check_password(password) : if user is not None and user.check_password(password) and user.is_authenticated: #user.is_authenticated是看他有沒有許可權的,這裡可以不加上它 return user
在配置檔案settings/dev.py中告知Django使用我們自定義的認證後端
AUTHENTICATION_BACKENDS = [ 'users.utils.UsernameMobileAuthBackend', ]
以上就實現了我們通過使用者名稱或者手機號的一個多條件登入。
5.登入狀態的判斷和退出登入
在Vheader元件中新增如下內容
<script> export default { name: "Header", data() { return { ..... token: true, ..... } }, methods: { ...... check_login() { // 二者只要其中之一有值就是true 代表使用者已經登入 this.token = localStorage.token || sessionStorage.token; }, logout() { // 使用者登出時,無論是臨時儲存還是永久儲存都應該清除掉 sessionStorage.removeItem('token'); sessionStorage.removeItem('username'); sessionStorage.removeItem('id'); localStorage.removeItem('token'); localStorage.removeItem('username'); localStorage.removeItem('id'); this.check_login(); } }, created() { this.get_nav_data(); }