后端API校验
基于drf的认证组件实现只有登录之后才能查看。
1.认证组件的开发
raise AuthenticationFailed()
return user_object, token
2.请求测试
认证失败
状态码:401
返回值:
{
"code": "1000",
"msg": "认证失败"
}
认证成功:
状态码:200
返回值:
{
"code": 0,
"msg": "认证失败"
}
auth.py
from rest_framework.authentication import BaseAuthentication
from rest_framework.exceptions import APIException, AuthenticationFailed
from api import models
# from rest_framework.views import exception_handler
from rest_framework import status
class MineAuthenticationFailed(AuthenticationFailed):
status_code = status.HTTP_200_OK
class MineAuthentication(BaseAuthentication):
def authenticate(self, request):
"""
None,交给后续的认证组件;
(user,token) request.user request.auth
异常AuthenticationFailed(...)
"""
# 1.获取请求传递token /api/xxxx?token=?
token = request.query_params.get("token")
if not token:
raise MineAuthenticationFailed("认证失败")
# raise MineAuthenticationFailed({"code": 1000, "msg": "认证失败"})
# raise AuthenticationFailed({"code": 1000, "msg": "认证失败"})
# 2.校验token合法性
user_object = models.UserInfo.objects.filter(token=token).first()
if not user_object:
raise MineAuthenticationFailed("认证失败")
# raise MineAuthenticationFailed({"code": 1000, "msg": "认证失败"})
# raise AuthenticationFailed({"code": 1000, "msg": "认证失败"})
# 3.认证成功 request.user request.auth
return user_object, token
def authenticate_header(self, request):
return "API"
生效
axios请求
axios请求(状态码)
后端API:AuthenticationFailed
前端请求:
- 成功 200 {code:0}
- 失败 401 {code:1000}
_axios.get("/api/vip/",)
.then((res) => {
console.log(res);
dataList.value = res.data.data
})
.catch((reason) => {
console.log("异常", reason)
})
.finally(() => {
console.log("结束")
})
后端API:MineAuthenticationFailed
前端请求:
- 成功 200 {code:0}
- 失败 200 {code:1000}
_axios.get("/api/vip/",)
.then((res) => {
console.log(res);
dataList.value = res.data.data
})
token携带
import axios from "axios";
import {userInfoStore} from "@/stores/counter.js";
import router from "@/router/index.js";
let config = {
baseURL
timeout: 20 * 1000
}
const _axios = axios.create(config)
_axios.interceptors.request.use(function (config) {
// 1.去pinia中读取当前用户token
const info = userInfoStore()
// 2.发送请求携带token
if (info.userToken) {
if (config.params) {
config.params['token'] = info.userToken
} else {
config.params = {token: info.userToken}
}
}
return config;
})
响应
_axios.interceptors.response.use(function (response) {
console.log("拦截器", response)
// 认证失败
if (response.data.code === "1000") {
router.replace({name: "login"})
}
return response;
}, function (error) {
// console.log("拦截器异常", error)
if (error.response.data.code === "1000") {
router.replace({name: "login"})
// return error; // 执行成功
}
//...
return Promise.reject(error); //想后续的异常去传递,执行下个异常
})
export default _axios;
返回格式定制
settings.py
REST_FRAMEWORK = {
"DEFAULT_AUTHENTICATION_CLASSES": ["utils.auth.MineAuthentication"],
# "EXCEPTION_HANDLER":"rest_framework.views.exception_handler",
"EXCEPTION_HANDLER": "utils.view.exception_handler",
}
#utils.view.py
from django.http import Http404
from rest_framework import exceptions
from rest_framework.response import Response
from rest_framework.exceptions import ValidationError
from rest_framework.exceptions import NotAuthenticated
from rest_framework.exceptions import AuthenticationFailed
from rest_framework.views import set_rollback
from rest_framework.viewsets import ModelViewSet
from rest_framework import status
class BaseView:
def finalize_response(self, request, response, *args, **kwargs):
response = super().finalize_response(request, response, *args, **kwargs)
print(response)
# 1.非正常
if response.exception:
print(1)
return response
# 2.正常
response.data = {"code": 0, "data": response.data}
response.status_code = status.HTTP_200_OK
return response
class MineModelViewSet(BaseView, ModelViewSet):
pass
def exception_handler(exc, context):
if isinstance(exc, Http404):
exc = exceptions.NotFound()
exc.ret_code = 1001
elif isinstance(exc, (AuthenticationFailed, NotAuthenticated)):
exc.ret_code = 1002
elif isinstance(exc, ValidationError):
exc.ret_code = 1003
exc.status_code = status.HTTP_200_OK
if isinstance(exc, exceptions.APIException):
headers = {}
if getattr(exc, 'auth_header', None):
headers['WWW-Authenticate'] = exc.auth_header
if getattr(exc, 'wait', None):
headers['Retry-After'] = '%d' % exc.wait
exc_code = getattr(exc, 'ret_code', None) or -1
data = {'code': exc_code, 'detail': exc.detail}
set_rollback()
return Response(data, status=exc.status_code, headers=headers)
# return None
data = {'code': -1, 'detail': str(exc)}
return Response(data, status=500)
vip展示
def post(self, request):
# 1.获取数据 request.data
# 2.校验
ser = VipSerializer(data=request.data)
ser.is_valid(raise_exception=True)
# if not ser.is_valid():
# return Response({"code": 1000, 'msg': "校验失败", "detail": ser.errors})
# # 3.保存
ser.save()
后端业务
vip2.py
import time
from rest_framework.views import APIView
from rest_framework import serializers
from rest_framework.response import Response
from api import models
from utils.view import BaseView, MineModelViewSet
from utils.exception import ExtraException
from rest_framework.viewsets import ModelViewSet
from rest_framework.exceptions import ValidationError
class VipSerializer(serializers.ModelSerializer):
level_text = serializers.CharField(source="get_level_display", read_only=True)
class Meta:
model = models.Vip
fields = "__all__"
def validate_name(self, value):
exists = models.Vip.objects.filter(name=value).exists()
if exists:
raise ValidationError("姓名已存在")
return value
# class VipView(BaseView, ModelViewSet):
class VipView(MineModelViewSet):
serializer_class = VipSerializer
queryset = models.Vip.objects.all().order_by("-id")
前端升级
flex布局
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<!-- <style>-->
<!-- .menu {-->
<!-- height: 100px;-->
<!-- background-color: #ddd;-->
<!-- display: flex;-->
<!-- }-->
<!-- </style>-->
</head>
<body>
<div class='menu'>
<div >英特尔</div>
<div >amd</div>
</div>
</body>
</html>
元素方向
<style>
.menu {
height: 100px;
background-color: #ddd;
display: flex;
flex-direction: row; /*主轴=横向, 或者是column*/
}
</style>
元素排列方式
<style>
.menu {
height: 100px;
background-color: #ddd;
display: flex;
flex-direction: row; /*主轴=横向, 或者是column*/
/*justify-content: space-evenly;*/
justify-content: space-between;
align-items: center;
/*align-items: flex-start;*/
/*align-items: flex-end;*/
}
.menu .item{
width: 45px;
height: 50px;
border: 1px solid green;
}
</style>
</head>
<body>
<div class='menu'>
<div class='item'>英特尔</div>
<div class='item'>amd</div>
<div class='item'>英伟达</div
</div>
</body>
换行
.menu {
height: 100px;
background-color: #ddd;
display: flex;
flex-direction: row; /*主轴=横向, 或者是column*/
justify-content: space-between;
align-items: center;
/*align-items: flex-start;*/
/*align-items: flex-end;*/
/*换行*/
flex-wrap: wrap;
}
elementplus
安装
npm install element-plus
比如引入按钮:
<template>
<h1>欢迎管理员:{{store.userName}}</h1>
<el-button type="primary">Primary</el-button>
<el-button type="danger">Danger</el-button>
<el-input placeholder="Please input" />
</template>
<script setup>
import {userInfoStore} from "@/stores/counter.js";
import { Delete, Edit, Search, Select, Upload } from '@element-plus/icons-vue'
const store = userInfoStore()
</script>
<style scoped>
</style>
关于UI框架
登录案例
在Form表单中查看,选择性地copy
<template>
<div class="box">
<el-form :model="form" label-position="top" :rules="formRules" ref="formRef">
<el-form-item label="用户名" :error="formError.username" prop="username">
<el-input v-model="form.username" placeholder="用户名"/>
</el-form-item>
<el-form-item label="密码" :error="formError.password" prop="password">
<el-input v-model="form.password" placeholder="密码"/>
</el-form-item>
<el-form-item>
<el-button type="primary" @click="doLogin">登 录</el-button>
</el-form-item>
</el-form>
</div>
</template>
<script setup>
import {ref} from "vue";
import {useRouter} from "vue-router"
import {userInfoStore} from "@/stores/counter.js";
import _axios from "@/plugins/axios.js";
import {ElMessage} from 'element-plus'
const router = useRouter()
const formRef = ref("")
const form = ref({
username: "root",
password: "123",
})
const formError = ref({
username: "",
password: "",
})
const formRules = ref({
username: [
{required: true, message: '用户名不能为空', trigger: 'blur'},
],
password: [
{required: true, message: '密码不能为空', trigger: 'blur'},
{min: 3, message: '密码长度不能小于3', trigger: 'blur'},
],
})
const error = ref("")
const store = userInfoStore()
function doLogin() {
formRef.value.validate((valid) => {
if (!valid) {
return false;
}
Object.keys(formError.value).forEach((k) => {
formError.value[k] = ""
})
_axios.post("/api/auth/", form.value).then((res) => {
if (res.data.code === 0) {
// {id: 1, name: username.value, token: "xxx88sdkweisdfsd"}
store.doLogin(res.data.data)
router.push({name: "home"})
} else if (res.data.code === 1003) {
Object.keys(res.data.detail).forEach((k) => {
formError.value[k] = res.data.detail[k][0]
})
} else if (res.data.code === -1) {
ElMessage.error(res.data.detail)
}
})
})
}
</script>
<style scoped>
.box {
width: 300px;
margin: 100px auto;
border: 1px solid #ddd;
padding: 20px 25px;
}
</style>
校验效果与提示
后台校验
utils.exceptions.py
from rest_framework.exceptions import APIException
from rest_framework import status
class ExtraException(APIException):
status_code = status.HTTP_200_OK
views/account.py
# 3.数据库校验
user_object = models.UserInfo.objects.filter(**ser.data).first()
if not user_object:
raise ExtraException("用户名或密码错误")
前端顶部导航
左侧菜单
<template>
<el-container>
<el-header height="72px" >
<div class="header">
<div class="logo">
<img src="../assets/logo.png" alt="">
</div>
<div class="toolbar">
<el-dropdown>
<el-icon >
<Setting/>
</el-icon>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item>View</el-dropdown-item>
<el-dropdown-item>Add</el-dropdown-item>
<el-dropdown-item @click="doLogout">退出登录</el-dropdown-item>
</el-dropdown-menu>
</template>
</el-dropdown>
<span>{{ store.userName }}</span>
</div>
</div>
</el-header>
<el-container class="main">
<el-aside width="300px">
<el-menu :router="true" :default-active="activeRouter">
<el-menu-item index="home" :route="{name:'home'}">
<el-icon>
<IconMenu/>
</el-icon>
<span>首页</span>
</el-menu-item>
<el-sub-menu index="user">
<template #title>
<el-icon>
<User/>
</el-icon>
<span>会员中心</span>
</template>
<el-menu-item index="vip" :route="{name:'vip'}">VIP管理</el-menu-item>
</el-sub-menu>
<!-- <el-sub-menu index="order">-->
<!-- <template #title>-->
<!-- <el-icon><User/></el-icon>-->
<!-- <span>会员中心</span>-->
<!-- </template>-->
<!-- <el-menu-item index="mine1" :route="{name:'vip'}">测试</el-menu-item>-->
<!-- <el-menu-item index="mine2" :route="{name:'vip'}">测试</el-menu-item>-->
<!-- </el-sub-menu>-->
</el-menu>
</el-aside>
<el-main class="body">
<router-view/>
</el-main>
</el-container>
</el-container>
</template>
<script setup>
import {Menu as IconMenu, Setting, User} from '@element-plus/icons-vue'
import {useRouter} from "vue-router";
import {useRoute} from "vue-router";
import {userInfoStore} from "@/stores/counter.js";
const store = userInfoStore()
const router = useRouter()
// 获取当前路由名称
// 设置activeRouter,刷新时页面默认选中某个菜单
const route = useRoute()
const activeRouter =
function doLogout() {
store.doLogout()
router.push({name: "login"})
}
</script>
<style scoped>
.header {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
height: 72px;
}
.header .logo {
height: 48px;
}
img {
height: 100%;
}
.header .toolbar {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
}
.main {
height: calc(100vh - 72px);
}
.body {
background-color: #f5f5f5;
}
</style>
vip管理
<el-card shadow="never">
<template #header>
<div class="card-header">
<span>会员管理</span>
</div>
<el-table :data="dataList" :border="true">
<el-table-column prop="id" label="ID"/>
<el-table-column prop="name" label="用户名"/>
<el-table-column prop="level_text" label="级别"/>
<el-table-column prop="score" label="积分"/>
</el-table>
</template>
</el-card>
完整版
增删改,确认删除
<template>
<el-card shadow="never">
<template #header>
<div class="card-header">
<span>会员管理</span>
</div>
</template>
<el-row >
<el-button type="primary" @click="doAdd">新增</el-button>
</el-row>
<el-table :data="dataList" :border="true" v-loading="loading">
<el-table-column prop="id" label="ID"/>
<el-table-column prop="name" label="姓名"/>
<el-table-column prop="level_text" label="级别"/>
<el-table-column prop="score" label="积分"/>
<el-table-column label="操作">
<template #default="scope">
<el-button size="small" @click="doEdit(scope.row.id, scope.$index)">编辑</el-button>
<el-button size="small" type="danger" @click="doDelete(scope.row.id, scope.$index)">删除</el-button>
<el-popconfirm
confirm-button-text="确认"
cancel-button-text="取消"
title="是否确定删除?"
@confirm="doDelete(scope.row.id, scope.$index)">
<template #reference>
<el-button size="small" type="danger">Delete</el-button>
</template>
</el-popconfirm>
</template>
</el-table-column>
</el-table>
<div >
<el-pagination
:total="page.totalCount"
:page-size="page.perPageSize"
background
layout="prev, pager, next,jumper"
@current-change="handleChangePage">
</el-pagination>
</div>
</el-card>
<el-dialog v-model="dialog" :title="dialogTitle"
:close-on-click-modal="false"
:close-on-press-escape="false"
:show-close="false"
width="500">
<el-form :model="form" label-position="top">
<el-form-item label="姓名" :error="">
<el-input v-model="" placeholder="姓名"/>
</el-form-item>
<el-form-item label="级别" :error="formError.level">
<el-select v-model="form.level">
<el-option v-for="item in levelList" :label="item.text" :value="item.id"/>
</el-select>
</el-form-item>
<el-form-item label="积分" :error="formError.score">
<el-input v-model="form.score" placeholder="积分"/>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="dialog=false;editId=-1;editIndex=-1">取 消</el-button>
<el-button type="primary" @click="doSave">提 交</el-button>
</div>
</template>
</el-dialog>
</template>
<script setup>
import {ref, onMounted} from "vue";
import _axios from "@/plugins/axios.js";
import {useRouter} from "vue-router"
const router = useRouter()
const page = ref({
totalCount: 32,
perPageSize: 10
})
function handleChangePage(num) {
fetchDataList(num)
}
const dialogTitle = ref("")
const loading = ref(false)
const levelList = ref([
{id: 1, text: "普通会员"},
{id: 2, text: "超级会员"},
{id: 3, text: "超超级会员"},
])
const dataList = ref([
// {id: 1, name: "武沛齐", level_text: "SVIP", score: 1000},
// {id: 1, name: "武沛齐", level_text: "SVIP", score: 1000},
// {id: 1, name: "武沛齐", level_text: "SVIP", score: 1000},
// {id: 1, name: "武沛齐", level_text: "SVIP", score: 1000},
])
const dialog = ref(false)
const form = ref({
name: "",
level: 1,
score: 100,
})
const formError = ref({
name: "",
level: "",
score: ""
})
const editId = ref(-1)
const editIndex = ref(-1)
function fetchDataList(num) {
loading.value = true
_axios.get("/api/vip/", {params: {page: num}}).then((res) => {
if (res.data.code === 0) {
dataList.value = res.data.data;
page.value = {
totalCount: res.data.data.totalCount,
perPageSize: res.data.data.perPageSize
}
}
}).finally(() => {
loading.value = false;
})
}
onMounted(function () {
fetchDataList(1)
})
function doAdd() {
dialog.value = true
dialogTitle.value = "新增VIP用户"
}
function doDelete(vid, idx) {
_axios.delete(`/api/vip/${vid}/`,).then((res) => {
// console.log(res);
if (res.data.code === 0) {
dataList.value.splice(idx, 1)
}
})
}
function doSave() {
// 清空错误信息
Object.keys(formError.value).forEach((k) => {
formError.value[k] = ""
})
if (editId.value > 0) {
// 更新
_axios.put(`/api/vip/${editId.value}/`, form.value).then((res) => {
// console.log(res.data); // {code:0,data:{...}}
if (res.data.code === 0) {
dataList.value[editIndex.value] = res.data.data;
dialog.value = false
editId.value = -1
editIndex.value = -1
form.value = {
name: "",
level: 1,
score: 100,
}
} else if (res.data.code === 1003) {
Object.keys(res.data.detail).forEach((k) => {
formError.value[k] = res.data.detail[k][0]
})
}
})
} else {
// 新增
_axios.post("/api/vip/", form.value).then((res) => {
console.log(res.data);
if (res.data.code === 0) {
//dataList.value.push(res.data.data)
dataList.value.unshift(res.data.data)
dialog.value = false
form.value = {
name: "",
level: 1,
score: 100,
}
} else if (res.data.code === 1003) {
Object.keys(res.data.detail).forEach((k) => {
formError.value[k] = res.data.detail[k][0]
})
}
})
}
}
function doEdit(vid, idx) {
dialogTitle.value = "修改VIP用户"
// console.log(vid, idx)
//1.默认值
let rowDict = dataList.value[idx]
form.value = {
name: ,
level: rowDict.level,
score: rowDict.score
}
//2.当期编辑ID
editId.value = vid
editIndex.value = idx
// 3.对话框展示
dialog.value = true
}
</script>
<style scoped>
.mask {
position: fixed;
top: 0;
bottom: 0;
right: 0;
left: 0;
background-color: black;
opacity: 0.8;
z-index: 998;
}
.dialog {
position: fixed;
top: 100px;
right: 0;
left: 0;
width: 400px;
height: 300px;
background-color: white;
margin: 0 auto;
z-index: 999;
}
</style>
新增的对话框
<el-dialog v-model="dialog" :title="dialogTitle"
:close-on-click-modal="false"
:close-on-press-escape="false"
:show-close="false"
width="500">
<el-form :model="form" label-position="top">
<el-form-item label="姓名" :error="">
<el-input v-model="" placeholder="姓名"/>
</el-form-item>
<el-form-item label="级别" :error="formError.level">
<el-select v-model="form.level">
<el-option v-for="item in levelList" :label="item.text" :value="item.id"/>
</el-select>
</el-form-item>
<el-form-item label="积分" :error="formError.score">
<el-input v-model="form.score" placeholder="积分"/>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="dialog=false;editId=-1;editIndex=-1">取 消</el-button>
<el-button type="primary" @click="doSave">提 交</el-button>
</div>
</template>
</el-dialog>
function doAdd() {
dialog.value = true
dialogTitle.value = "新增VIP用户"
}
分页
限制发五条:
这里是三个data,
loading.value = true
_axios.get("/api/vip/", {params: {page: num}}).then((res) => {
if (res.data.code === 0) {
dataList.value = res.data.data.data;
page.value = {
totalCount: res.data.data.totalCount,
perPageSize: res.data.data.perPageSize
}
}
}).finally(() => {
loading.value = false;
})
效果
<div >
<el-pagination
:total="page.totalCount"
:page-size="page.perPageSize"
background
layout="prev, pager, next,jumper"
@current-change="handleChangePage">
</el-pagination>
</div>
function handleChangePage(num) {
fetchDataList(num)
}