1、功能完善(简易版)
1.1 后端API校验
基于drf的认证组件实现只有登录之后才能查看
utils/auth.py
from rest_framework.authentication import BaseAuthentication
from rest_framework.exceptions import APIException, AuthenticationFailed
from rest_framework import status
from api import models
class MineAuthenticationFailed(APIException):
status_code = status.HTTP_200_OK
class MineAuthentcation(BaseAuthentication):
def authenticate(self, request):
token = request.query_params.get("token")
if not token:
raise MineAuthenticationFailed("token不存在")
user_object = models.UserInfo.objects.filter(token=token).first()
if not user_object:
raise MineAuthenticationFailed("认证失败")
return user_object, token
settings.py
REST_FRAMEWORK = {
"DEFAULT_AUTHENTICATION_CLASSES": ["utils.auth.MineAuthentcation"],
}
account.py
class AuthView(MineApiView):
authentication_classes = []
1.2 前端校验
axios.js
const _axios = axios.create(config)
const info = userInfoStore()
_axios.interceptors.request.use(function (config) {
// console.log("请求拦截器:", config)
// 1. 去pinia中读取当前用户token
// 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 Promise.reject(error)
})
export default _axios
LoginView.vue
const doLogin = function () {
// 1、获取数据
console.log(msg.value)
// 2、发送网络请求
_axios.post("/api/auth/", msg.value).then((res) => {
console.log(res.data)
if (res.data.code === 0){
store.doLogin(res.data.data)
router.push({name: "home"})
} else {
error.value = res.data.msg
setTimeout(function (){
error.value=""
}, 5000)
}
})
}
2、后端API升级
2.1 正常请求返回
utils/views.py
class BaseAPIView:
def finalize_response(self, request, response, *args, **kwargs):
response = super().finalize_response(request, response, *args, **kwargs)
# 1. 非正常
if response.exception:
return response
# 2. 正常
response.data = {"code": 0, "data": response.data}
return response
vip.py
from rest_framework.views import APIView
from api import models
from rest_framework import serializers
from rest_framework.response import Response
from rest_framework.exceptions import ValidationError
from utils.exc_views import ExtraExceptions
from utils.views import BaseAPIView
class VipSerializers(serializers.ModelSerializer):
level_text = serializers.CharField(source="get_level_display", read_only=True)
class Meta:
model = models.Vip
fields = "__all__"
class VipView(BaseAPIView, APIView):
def get(self, request):
# 会员列表
queryset = models.Vip.objects.all().order_by("id")
ser = VipSerializers(instance=queryset, many=True)
return Response(ser.data)
def post(self, request):
""" 新增 """
# 1.获取数据 request.data
# 2.校验数据
ser = VipSerializers(data=request.data)
print(request.data)
exist = models.Vip.objects.filter(name=request.data["name"]).exists()
if exist:
raise ValidationError({"msg": "会员已存在"})
if not ser.is_valid():
raise ValidationError({"msg": "校验失败", "detail": ser.errors})
# 3.保存
ser.save()
# 4.返回
return Response(ser.data)
class VipDetailView(BaseAPIView, APIView):
def delete(self, request, vid):
# 删除 ?返回已删除的数据
models.Vip.objects.filter(id=vid).delete()
return Response({"msg": "删除成功"})
def put(self, request, vid):
""" 修改 """
# 1.获取数据 request.data
instance = models.Vip.objects.filter(id=vid).first()
""" BUG:id不存在时,会进行新增操作 """
if not instance:
raise ExtraExceptions("id不存在,无法更新")
# 2.校验数据
ser = VipSerializers(data=request.data, instance=instance)
if not ser.is_valid():
raise ValidationError({"msg": "校验失败", "detail": ser.errors})
# 3.保存
ser.save()
# 4.返回
return Response({"data": ser.data})
2.2 异常请求返回
utils/views.py
class ExtraExceptions(APIException):
pass
def exception_handler(exc, context):
if isinstance(exc, Http404):
exc.ret_code = 1001
exc = exceptions.NotFound(*exc.args)
elif isinstance(exc, PermissionDenied):
exc.ret_code = 1002
exc = exceptions.PermissionDenied(*exc.args)
elif isinstance(exc, (AuthenticationFailed, NotAuthenticated)):
exc.ret_code = 1003
elif isinstance(exc, Throttled):
exc.ret_code = 1004
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)
data = {"code": -1, "detail": str(exc)}
return Response(data, status=500)
vip.py
from api import models
from rest_framework.views import APIView
from rest_framework import serializers
from rest_framework.response import Response
from rest_framework.exceptions import ValidationError
from utils.views import BaseAPIView, ExtraExceptions
class VipSerializers(serializers.ModelSerializer):
level_text = serializers.CharField(source="get_level_display", read_only=True)
class Meta:
model = models.Vip
fields = "__all__"
class VipView(BaseAPIView, APIView):
def get(self, request):
# 会员列表
queryset = models.Vip.objects.all().order_by("id")
ser = VipSerializers(instance=queryset, many=True)
return Response(ser.data)
def post(self, request):
""" 新增 """
# 1.获取数据 request.data
# 2.校验数据
ser = VipSerializers(data=request.data)
print(request.data)
exist = models.Vip.objects.filter(name=request.data["name"]).exists()
if exist:
raise ValidationError({"msg": "会员已存在"})
if not ser.is_valid():
raise ValidationError({"msg": "校验失败", "detail": ser.errors})
# 3.保存
ser.save()
# 4.返回
return Response(ser.data)
class VipDetailView(BaseAPIView, APIView):
def delete(self, request, vid):
# 1.获取数据 request.data
instance = models.Vip.objects.filter(id=vid).first()
if not instance:
raise ExtraExceptions("id不存在,无法删除")
# 删除 ?返回已删除的数据
models.Vip.objects.filter(id=vid).delete()
return Response({"msg": "删除成功"})
def put(self, request, vid):
""" 修改 """
# 1.获取数据 request.data
instance = models.Vip.objects.filter(id=vid).first()
""" BUG:id不存在时,会进行新增操作 """
if not instance:
raise ExtraExceptions("id不存在,无法更新")
# 2.校验数据
ser = VipSerializers(data=request.data, instance=instance)
if not ser.is_valid():
raise ValidationError({"msg": "校验失败", "detail": ser.errors})
# 3.保存
ser.save()
# 4.返回
return Response(ser.data)
account.py
import uuid
from rest_framework import serializers
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.exceptions import AuthenticationFailed
from api import models
from utils.views import BaseAPIView
class AuthSerializer(serializers.Serializer):
username = serializers.CharField(required=True)
password = serializers.CharField(required=True)
class AuthView(BaseAPIView, APIView):
authentication_classes = []
def post(self, request):
# 1. 获取用户提交数据 request.data = {"username": "xxx", "password": "...}
# 2. 表单校验
ser = AuthSerializer(data=request.data)
# if not ser.is_valid():
# raise ValidationError({"msg": "校验失败", "detail": ser.errors})
ser.is_valid(raise_exception=True)
# 3. 数据库校验
user_object = models.UserInfo.objects.filter(**ser.data).first()
if not user_object:
raise AuthenticationFailed("用户名或密码错误")
token = uuid.uuid4()
user_object.token = token
user_object.save()
# 4. 数据返回
return Response({"id": user_object.id, "name": user_object.username, "token": user_object.token})
2.3 业务功能开发(路由+视图+序列化器)
urls.py
from django.urls import path
from api.views import account
from api.views import vip, vip2
from rest_framework import routers
router = routers.SimpleRouter()
router.register(r"api/vip2", vip2.VipView)
urlpatterns = [
path('api/auth/', account.AuthView.as_view()),
path('api/vip/', vip.VipView.as_view()),
path('api/vip/<int:vid>/', vip.VipDetailView.as_view()),
]
urlpatterns += router.urls
utils/pagination.py
from rest_framework.pagination import PageNumberPagination
from rest_framework.response import Response
class MinePageNumberPagination(PageNumberPagination):
def get_paginated_response(self, data):
return Response({
'totalCount': self.page.paginator.count,
'perpageCount': self.page_size,
'results': data,
})
settings.py
REST_FRAMEWORK = {
"EXCEPTION_HANDLER": "utils.views.exception_handler",
"DEFAULT_AUTHENTICATION_CLASSES": ["utils.auth.MineAuthentcation"],
"DEFAULT_PAGINATION_CLASS": "utils.pagination.MinePageNumberPagination",
"PAGE_SIZE": 5,
}
vip2.py
from rest_framework.viewsets import ModelViewSet
from api import models
from rest_framework import serializers
from utils.views import BaseAPIView
class VipSerializers(serializers.ModelSerializer):
level_text = serializers.CharField(source="get_level_display", read_only=True)
class Meta:
model = models.Vip
fields = "__all__"
class VipView(BaseAPIView, ModelViewSet):
serializer_class = VipSerializers
queryset = models.Vip.objects.all().order_by("id")
3、前端页面升级
3.1 Flex布局
在父级标签中,添加flex样式
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Vite App</title>
<style>
.menu {
height: 100px;
background-color: #ddd;
display: flex;
/* 设置主轴方向,row:横向,colume:纵向*/
flex-direction: row;
/* 主轴方向元素排列,flex-start,flex-end,center,space-between*/
justify-content: space-between;
/* 副轴方向元素排列: stretch,center,end*/
align-items: end;
/* 多行换行,默认不换行*/
flex-wrap: wrap;
}
</style>
</head>
<body>
<div class="menu">
<div style="background-color: cornflowerblue">11</div>
<div style="background-color: pink">22</div>
<div style="background-color: cornflowerblue">33</div>
</div>
</body>
</html>
3.2 Element Plus
官网链接:https://element-plus.org/zh-CN/guide/quickstart.html
安装:npm install element-plus --save
引入配置:main.js
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
app.use(ElementPlus)
登录页:LoginView.vue
<template>
<div class="box">
<el-form :model="msg" label-position="top">
<el-form-item label="用户名" :error="msgError.username">
<el-input v-model="msg.username"/>
</el-form-item>
<el-form-item label="密码" :error="msgError.password">
<el-input v-model="msg.password"/>
</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/user.js";
import _axios from "@/plugins/axios.js";
import { ElMessage } from 'element-plus'
const msg = ref({
username: "root",
password: "123"
})
const router = useRouter()
const store = userInfoStore()
const msgError = ref({
username: "",
password: ""
})
const doLogin = function () {
// 1、获取数据
console.log(msg.value)
Object.keys(msgError.value).forEach( (k)=>{msgError.value[k] = ""})
// 2、发送网络请求
_axios.post("/api/auth/", msg.value).then((res) => {
console.log(res.data)
if (res.data.code === 0) {
store.doLogin(res.data.data)
router.push({name: "home"})
} else if (res.data.code === 1003) {
Object.keys(res.data.detail).forEach( (k)=>{msgError.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;
}
</style>
后端:utils/views.py
from rest_framework import status
def exception_handler(exc, context):
elif isinstance(exc, (AuthenticationFailed, NotAuthenticated)):
exc.ret_code = 1002
exc.status_code = status.HTTP_200_OK
elif isinstance(exc, ValidationError):
exc.ret_code = 1003
exc.status_code = status.HTTP_200_OK
account.py
import uuid
from rest_framework import serializers
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.exceptions import ValidationError
from api import models
from utils.views import BaseAPIView, ExtraExceptions
class AuthSerializer(serializers.Serializer):
username = serializers.CharField(required=True)
password = serializers.CharField(required=True)
class AuthView(BaseAPIView, APIView):
authentication_classes = []
def post(self, request):
# 1. 获取用户提交数据 request.data = {"username": "xxx", "password": "...}
# 2. 表单校验
ser = AuthSerializer(data=request.data)
# if not ser.is_valid():
# raise ValidationError({"msg": "校验失败", "detail": ser.errors})
ser.is_valid(raise_exception=True)
# 3. 数据库校验
user_object = models.UserInfo.objects.filter(**ser.data).first()
if not user_object:
# raise ExtraExceptions("用户名或密码错误")
raise ValidationError({"password": ["用户名或密码错误"]})
token = uuid.uuid4()
user_object.token = token
user_object.save()
# 4. 数据返回
return Response({"id": user_object.id, "name": user_object.username, "token": user_object.token})
AdminView.vue
<template>
<el-container>
<el-header height="48px" style="border-bottom: 1px solid #f5f5f5">
<div class="header">
<div class="logo">
<img src="../assets/logo.svg">
</div>
<div class="toolbar">
<el-dropdown>
<el-icon style="margin-right: 8px; margin-top: 1px">
<setting/>
</el-icon>
<template #dropdown>
<el-dropdown-menu>
<el-dropdown-item @click="doLogout">退出登录</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>
<el-aside width="300px">
<el-menu :router="true" :default-active="activeRouter">
<el-menu-item index="home" :route="{name:'home'}">
<el-icon>
<icon-menu/>
</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-menu>
</el-aside>
<el-main>
<RouterView/>
</el-main>
</el-container>
</el-container>
</template>
<script setup>
import {useRouter} from "vue-router";
import {useRoute} from "vue-router";
import {userInfoStore} from "@/stores/user.js";
import {Setting, Menu as IconMenu, User} from '@element-plus/icons-vue'
import App from "@/App.vue";
const router = useRouter()
const route = useRoute()
const store = userInfoStore()
// 获取当前路由的名称,刷新时页面默认选中某个菜单
const activeRouter = route.name
function doLogout() {
store.doLogout()
router.push({name: "login"})
}
</script>
<style scoped>
body {
margin: 0;
}
.header {
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
height: 48px;
}
.header .logo {
height: 36px;
}
img {
height: 100%;
}
.header .toolbar {
display: flex;
flex-direction: row;
justify-content: center;
align-items: center;
}
</style>
2.3 右侧内容
VipView.vue
<template>
<el-card>
<template #header>
<div class="card-header">
<span>会员管理</span>
</div>
</template>
<el-row style="margin-bottom: 10px">
<el-button type="primary" @click="doAdd">新增</el-button>
</el-row>
<el-table :data="dataList" stripe border>
<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>
</el-card>
<el-dialog v-model="dialog" :title="dialogTitle" width="500"
:close-on-click-modal="false" :close-on-press-escape="false" :show-close="false">
<!-- <el-form model="form">-->
<!-- <el-form-item label="姓名"><el-input v-model="form.name" /></el-form-item>-->
<!-- <el-form-item label="积分"><el-input v-model="form.score" /></el-form-item>-->
<!-- </el-form>-->
<el-form :model="form">
<el-form-item label="姓名" :error="formError.name">
<el-input v-model="form.name"/>
</el-form-item>
<el-form-item label="级别" :error="formError.level">
<el-select v-model="form.level" placeholder="Select" size="default" style="width: 240px">
<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"/>
</el-form-item>
</el-form>
<template #footer>
<div class="dialog-footer">
<el-button @click="dialog=false; editId=-1; editIdx=-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";
// const dataList = ref([{id: 1, name: "cc", age: 18, level_text: "SVIP", score: 1000}])
const dataList = ref([{}])
const dialog = ref(false)
const dialogTitle = ref("")
const levelList = ref([
{id: 1, "text": "普通会员"},
{id: 2, "text": "超级会员"},
{id: 3, "text": "超超级会员"},
])
const form = ref({
name: "",
level: "",
score: ""
})
const editId = ref(-1)
const editIdx = ref(-1)
const loading = ref(false)
const formError = ref({
name: "",
level: "",
score: ""
})
onMounted(function () {
loading.value = true
_axios.get("/api/vip/").then((res) => {
if (res.data.code === 0) {
dataList.value = res.data.data
} else if (res.data.code === "1000") {
console.log("认证失败", res.data)
}
}).finally(() => {
loading.value = false
})
})
function doAdd() {
dialogTitle.value = "新增会员"
dialog.value = true
form.value = ({
name: "",
level: 1,
score: 100
})
formError.value = ({
name: "",
level: "",
score: ""
})
}
function doSave() {
if (editId.value === -1) {
// 新增
_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) {
formError.value = res.data.detail
} else if (res.data.code === -1) {
formError.value.name = res.data.detail
}
})
} else {
// 更新
_axios.put(`/api/vip/${editId.value}/`, form.value).then((res) => {
if (res.data.code === 0) {
dataList.value[editIdx.value] = res.data.data
dialog.value = false
form.value = {
name: "",
level: 1,
score: 100
}
editId.value = -1
editIdx.value = -1
} else if (res.data.code === 1003) {
formError.value = res.data.detail
} else if (res.data.code === -1) {
formError.value.name = res.data.detail
}
})
}
}
function doEdit(vid, idx) {
// 显示编辑的值
formError.value = ({
name: "",
level: "",
score: ""
})
dialogTitle.value = "编辑会员信息"
let editDict = dataList.value[idx]
form.value = {
name: editDict.name,
level: editDict.level,
score: editDict.score
}
// 当前编辑行的id
editId.value = vid
editIdx.value = idx
dialog.value = true
}
function doDelete(vid, idx) {
_axios.delete(`/api/vip/${vid}/`).then((res) => {
// console.log(res.data)
if (res.data.code === 0) {
dataList.value.splice(idx, 1)
}
})
}
</script>
<style scoped>
.mask {
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
background-color: black;
opacity: 0.8;
z-index: 998;
}
.dialog {
position: fixed;
top: 200px;
right: 0;
left: 0;
width: 400px;
height: 300px;
background-color: white;
margin: 0 auto;
z-index: 9999;
}
</style>
1193

被折叠的 条评论
为什么被折叠?



