【VUE+DRF】案例升级

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>
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值