后端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"
生效

drf配合vue,elementplus的简单使用_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: "http://127.0.0.1:8000",
    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;
})

drf配合vue,elementplus的简单使用_ios_02

响应
_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;

drf配合vue,elementplus的简单使用_ios_03

返回格式定制

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)

drf配合vue,elementplus的简单使用_API_04

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()

drf配合vue,elementplus的简单使用_API_05

后端业务

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 style="background-color: palegreen">英特尔</div>
    <div style="background-color: gold">amd</div>

</div>
</body>
</html>

drf配合vue,elementplus的简单使用_html_06

drf配合vue,elementplus的简单使用_html_07

元素方向

    <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>

drf配合vue,elementplus的简单使用_API_08

换行

  .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

https://element-plus.org/zh-CN/component/layout.html

安装
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 style="width: 240px" 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>

drf配合vue,elementplus的简单使用_ios_09

关于UI框架

登录案例

在Form表单中查看,选择性地copy

drf配合vue,elementplus的简单使用_html_10


<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>

drf配合vue,elementplus的简单使用_ios_11

校验效果与提示

drf配合vue,elementplus的简单使用_API_12

drf配合vue,elementplus的简单使用_API_13

后台校验

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("用户名或密码错误")

前端顶部导航

左侧菜单

drf配合vue,elementplus的简单使用_API_14

<template>

  <el-container>
    <el-header height="72px" style="border-bottom: 1px solid #f5f5f5;">
      <div class="header">
        <div class="logo">
          <img src="../assets/logo.png" alt="">
        </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>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 = route.name

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>

drf配合vue,elementplus的简单使用_ios_15

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>

drf配合vue,elementplus的简单使用_html_16

完整版

增删改,确认删除

<template>

  <el-card shadow="never">
    <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" :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 style="margin-top: 20px;">
      <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="formError.name">
        <el-input v-model="form.name" 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: rowDict.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>

drf配合vue,elementplus的简单使用_ios_17

新增的对话框

drf配合vue,elementplus的简单使用_API_18

<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="formError.name">
        <el-input v-model="form.name" 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用户"
}

分页

限制发五条:

drf配合vue,elementplus的简单使用_API_19

这里是三个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;
  })

drf配合vue,elementplus的简单使用_html_20

效果

drf配合vue,elementplus的简单使用_ios_21

<div style="margin-top: 20px;">
      <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)
}