自动化测试平台(六):后端增加分页功能及前端菜单栏和用户列表的实现

x33g5p2x  于2021-12-15 转载在 其他  
字(11.1k)|赞(0)|评价(0)|浏览(577)

一、前言

上一章节我们完成了前端基础框架的搭建以及结合接口完成了登录功能。这篇将实现用户的管理模块功能。

实现效果如下:

在线演示地址:http://121.43.43.59/ (帐号:admin 密码:123456)

二、创建项目文件及开发路由菜单

1. 创建主页和用户模块文件

1)修改登录页路由

根据上一篇完成的登录功能来看,我们的项目主页(url不带后缀的访问)就是登录页面,这实际上是不合理的,登录页应该通过类似http://localhost:8000/login这样的地址访问。
所以我们要修改登录页面的路由,在项目根目录的.umirc文件中将原来的登录页path由/改为/login

routes: [
    {
     - path: '/', //删除该行
     + path:'/login', //新增该行
      component: '@/pages/login'
    }],

保存并重启项目后,我们访问http://localhost:8000/login 才会看到登录页了。

2)创建主页及用户管理模块文件

在cmd中执行下面的命令来创建主页和用户管理模块文件:
npx umi g page index/index --typescript
npx umi g page user/index --typescript

创建完成的目录结构:

2. 配置路由和菜单

1)简单介绍

这里的路由菜单使用了prolayout组件,它能够很方便的为我们建设好看实用的菜单:

官方文档地址:https://prolayout.ant.design/getting-started

2)开发生成路由菜单代码

1.首先建立匹配显示菜单图标的文件,用于显示上图中菜单左侧的图标:

src文件夹下新建utils文件夹,并在它下面创建文件icon.tsx ,该文件用于将路由字符串转换为具体的路由图标,icon.tsx代码如下:

import { HomeOutlined, TeamOutlined, SettingFilled } from '@ant-design/icons';

// 菜单图标
export const IconMaps = {
    home: <HomeOutlined />,
    setting: <SettingFilled />,
    users: <TeamOutlined />
};

后面菜单需要使用新的图标时,都需要在这里进行加入

2.在src目录下创建一个名为layouts的文件夹,并在它的目录下创建index.tsx文件:

将下面的生成路由菜单代码放入index.tsx中:

import React from 'react';
import ProLayout, { PageContainer, MenuDataItem } from '@ant-design/pro-layout';
import { IconMaps } from '../utils/icon';
import { Link } from 'umi';

const BasicLayout: React.FC<{}> = (props: any) => {
    const { route } = props;
    // 菜单
    const loopMenuItem = (menus: MenuDataItem[]): MenuDataItem[] =>
        menus.map(({ icon, children, ...item }) => ({
            ...item,
            icon: IconMaps[icon as string],
            children: children && loopMenuItem(children),
        }));
    return (
        <ProLayout
            logo={false}
            siderWidth={220}
            title="曲鸟自动化测试平台"
            pageTitleRender={false}
            rightContentRender={() => (
                <h1>你好,曲鸟</h1>
            )}
            contentStyle={{ height: 'calc(100vh - 100px)' }}
            menuDataRender={() => loopMenuItem(route.routes)}
            menuItemRender={(item, dom) => (
                <Link to={item.path ?? '/'}>
                    {dom}
                </Link>
            )}
        >
            <PageContainer>{props.children}</PageContainer>
        </ProLayout>
    );
};

export default BasicLayout;

注:如果有波浪号报错,在tsconfig.json文件中的compilerOptions加入该项配置:
"suppressImplicitAnyIndexErrors": true, //减少部分警告

3)配置路由

umirc.ts文件中加入主页和用户页的路由,完整的代码如下:

import { defineConfig } from 'umi';
export default defineConfig({
  nodeModulesTransform: {
    type: 'none',
  },
  title: '曲鸟自动化测试平台', //改变浏览器title
  proxy: {
    '/api': {
      target: 'http://121.43.43.59:8001/',
      changeOrigin: true,
      pathRewrite: { '^/api': '' },
    },
  },
  routes: [
    {
      path: '/login', 
      component: '@/pages/login'
    },
    //---下面的为新增部分
    {
      path: '/',
      component: '@/layouts',
      routes: [
        {
          name: '主页',
          path: '/',
          icon: 'home',
          component: '@/pages/index',
        },
        {
          name: '用户管理',
          path: '/user',
          icon: 'users',
          component: '@/pages/user',
        },
      ],
    },
    //---上面的为新增部分
  ],
  fastRefresh: {},
});

保存文件,重启项目,访问http://localhost:8000/就可以看到成功配置后的菜单,并能够顺利的切换菜单页面:

三、后端增加筛选、分页功能和接口返回格式封装

为了能够更快速的开发这个项目(CURD),我们需要修改django接口返回数据的格式以至于适配proTable(一个非常方面的表格轮子)定义的返回格式。

1. 通过django中间件修改接口返回格式

1)我们可以直接在中间件中来拦截修改接口的返回格式,首先在django姓名下的QNtest文件夹中创建middleware.py文件,并放入下面的代码:

from django.utils.deprecation import MiddlewareMixin
from rest_framework import status

class ResponseMiddleWare(MiddlewareMixin):
    async_capable = True
    sync_capable = True

    def process_response(self, request, response):
        """ 处理响应格式 """
        if hasattr(response, 'data'):
            res_data = response.data
            if res_data is not None:  # delete请求返回的数据没有data,所以下面的逻辑需要判断有data
                if 'code' not in res_data:
                    if 'data' in res_data:
                        response.data['code'] = status.HTTP_200_OK
                    else:
                        response.data = {'code': status.HTTP_200_OK,
                                         'msg': res_data.pop('msg') if res_data.get('msg') else '',
                                         'data': res_data}
            else:
                setattr(response, 'data', {'code': status.HTTP_200_OK})
            # 状态码为2开头的都视为成功,否则都是失败
            response.data['success'] = True if str(response.status_code).startswith('2') else False
            response.content = response.rendered_content
        return response

2)然后在settings.py文件中的MIDDLEWARE下新增下面的配置:

'QNtest.middleware.ResponseMiddleWare',

修改前,请求列表的返回结果格式:

[]

修改前,请求列表的返回结果格式:

{ "code": 200, "data": [], "msg":"", "success": true }

2. 接口增加分页功能

有些小伙伴会发现,我们之前写的用户接口是没有分页功能的,这里我们需要增加分页功能。

djangorestframework(后面简称为 DRF)提供了很方便的分页功能:PageNumberPagination。但我们需要对其进行一些封装才能达到我们想要的分页效果:

1)首先我们在QNtest文件夹下新增pagination文件,并加入下面封装好的分页处理代码:

from rest_framework.exceptions import NotFound
from rest_framework.pagination import PageNumberPagination
from rest_framework.response import Response
from collections import OrderedDict

class StandardPageNumberPagination(PageNumberPagination):
    """ 自定义查询,前端可以自定义页码大小 """
    page_size_query_param = 'page_size'
    max_page_size = 10000  # 最大允许的数量

    def paginate_queryset(self, queryset, request, view=None):
        """ Paginate a queryset if required, either returning a page object, or `None` if pagination is not configured for this view. """
        print(self.page_size_query_param)
        try:
            return super().paginate_queryset(queryset, request, view)
        except NotFound as e:
            msg = '分页无效'
            raise NotFound(msg)

    def get_paginated_response(self, data):
        return Response(OrderedDict([
            ('total', self.page.paginator.count),
            ('data', data)
        ]))

2)然后在settings.py文件中的REST_FRAMEWORK下新增下面的配置:

'DEFAULT_PAGINATION_CLASS': 'QNtest.pagination.StandardPageNumberPagination',

这样配置完成后,通过DRF的视图实现的查询列表接口就都有了分页的功能了。

3. 用户查询接口增加筛选功能

增加按字段筛选功能,django-filter提供了很方便的方法,两行代码就可以搞定:
1)安装django-filter

pip install django-filter

2)在user视图类中加入下面的代码:

filter_backends = (DjangoFilterBackend,)
    filter_fields = ('is_active',)

上述代码表示前端可以指定is_active字段来进行筛选数据

4. 接口异常情况下返回数据的处理

1)通过上面的代码,我们增加了分页的功能,但实际应用的时候会出现一些异常情况,导致返回的数据格式不是我们想要的,比如表中只有两个用户,但我们要请求第100页的数据:page=100&page_size=2,这种情况下就会报错:

{
    "code": 200,
    "data": {
        "detail": "分页无效"
    },
    "success": true
}

实际前端可能希望我们在查询不出数据的时候返回一个空列表,类似下面这样:

{
    "code": 200,
    "data":[],
    "success": true
}

所以我们还得增加异常情况下的接口返回数据封装处理:

2)在QNtest文件夹下增加exception_handler文件并复制下面的代码:

from rest_framework import status
from rest_framework.views import exception_handler

def response_exception_handler(exc, context):
    """ 接口响应结果的异常处理 """
    # Call REST framework's default exception handler first,
    # to get the standard error response.

    response = exception_handler(exc, context)

    # Now add the HTTP status code to the response.
    if response is not None:

        res_data = response.data
        if isinstance(response.data, dict):
            if res_data.get('detail') == '分页无效':
                response.data['code'] = response.status_code = status.HTTP_200_OK
                response.data['total'] = 0
                response.data.pop("detail")
                response.data['data'] = []
            else:
                response.data['code'] = response.status_code
                if res_data.get('detail'):  # 可能存在报错没有detail的情况
                    response.data['msg'] = response.data.pop('detail')
                elif response.status_code == status.HTTP_400_BAD_REQUEST:
                    response.data['msg'] = "参数错误!"
        elif isinstance(response.data, (list, tuple)):
            data = response.data
            response.data = {'code': response.code, 'msg': ''.join(data)}
    return response

3)然后在settings.py文件中的REST_FRAMEWORK下新增下面的配置:

'EXCEPTION_HANDLER': 'QNtest.exception_handler.response_exception_handler'

这样再执行上述参数的接口查询时就不会报错了,而是返回下面这样的数据:

{"code":200,"total":0,"data":[],"msg":"","success":true}

四、开发用户列表页面并对接接口

1. 添加接口和公共接口封装

跟登录接口一样,我们需要新增一起请求用户列表的接口,但由于我们之前实现的接口是一个地址因请求方法不同使用的功能就不同(一个接口实现增删改查)所以我们要对类似的请求做一个封装。

1)在src文件夹下新增globalEnum.ts(全局变量)文件并加入下面的代码:

export const reqList = 'get-list'; //代表请求列表
export const reqDetail = 'get-detail'; //代表请求数据详情
export const reqDelete = 'delete'; //代表删除数据
export const reqCreate = 'create'; //代表创建数据
export const reqUpdate = 'update'; //代表修改数据

2)在src下创建一个utils文件夹(这里面会存放公共的方法等复用性比较高的方法)并在它下面新增comRequests.tsx文件,再加入下面的代码:

import { request } from 'umi';
import {
  reqList,
  reqDetail,
  reqCreate,
  reqUpdate,
  reqDelete,
} from '@/globalEnum';
export function comRequest(url: string, params: any, type: string) {
  switch (type) {
    /* eslint no-case-declarations:0 */
    case reqList:
      return request<any>(url, { params });
    case reqDetail:
      return request<any>(url + `/${params}`);
    case reqCreate:
      return request<any>(url, {
        method: 'POST',
        data: params,
      });
    case reqUpdate:
      const _id = params?.id || null
      delete params.id
      return request<any>(url + `/${_id}`, {
        method: 'PATCH',
        data: params,
      });
    case reqDelete:
      return request<any>(url + `/${params}`, { method: 'DELETE' });
    default:
      return request<any>(url, {
        method: 'POST',
        data: params,
      });
  }
}

上面的代码会根据传递的type来判断请求方法传递什么,来达到控制执行增删改查操作的目的。

2. 修改登录页代码

1)因为接口返回格式做了变更,并且需要增加登录成功后跳转到首页的功能,所以需要修改登录模块下的onFinish方法为下面这样:

const onFinish = (values: any) => {
    localStorage.setItem('token', '');
    services.login({ username: values.username, password: values.password }).then((res) => {
      localStorage.setItem('token', res.data.token);
      history.push('/');
    });
  };

3. 配置UMI拦截器实现请求带token

1)用户相关的接口是需要用户成功登录才能请求成功的,也就是验证请求header中的token是否有效,所以我们需要在app.tsxrequest中增加下面的代码:

requestInterceptors: [
    function authHeaderInterceptor(url: string, options: RequestOptionsInit) {
      const authHeader = {
        Authorization: localStorage.getItem('token') ? `Token ${localStorage.getItem('token')}` : '',
      };
      return {
        url: `${url}`,
        options: { ...options, interceptors: true, headers: { ...options.headers, ...authHeader } },
      };
    },
  ],

上面的代码是一个请求拦截器,UMI请求拦截器官方文档:https://umijs.org/zh-CN/plugins/plugin-request#requestinterceptors

4. 开发用户列表功能

用户列表使用了ProTable组件,官方文档:https://protable.ant.design/getting-started

pages/user/index.tsx文件中加入下面的代码:

import React, { useState } from 'react';
import ProTable, { ProColumns } from '@ant-design/pro-table';
import { PlusOutlined } from '@ant-design/icons';
import { Button, message } from 'antd';
import { reqList } from '@/globalEnum';
import { UserAll } from '@/services/users';

const columns: ProColumns[] = [
  {
    title: '姓名',
    dataIndex: 'name',
    key: 'name',
    width: 100,
    render: (v: any, record: any) => (
      <span>{`${record.last_name}${record.first_name}`}</span>
    ),
  },
  {
    title: '用户名',
    key: 'username',
    width: 100,
    dataIndex: 'username',
  },
  {
    title: '邮箱地址',
    key: 'email',
    dataIndex: 'email',
    width: 200,
  },
  {
    title: '加入时间',
    key: 'date_joined',
    dataIndex: 'date_joined',
    width: 200,
    valueType: 'dateTime',
  },
  {
    title: '状态',
    dataIndex: 'is_active',
    width: 100,
    filters: true,
    valueEnum: {
      true: { text: '启用', status: 'Success' },
      false: { text: '禁用', status: 'Error' },
    },
  },
  {
    title: '操作',
    key: 'option',
    width: 120,
    valueType: 'option',
    render: () => [<a>操作</a>, <a>删除</a>],
  },
];

const User: React.FC = () => {
  const [params, setParams] = useState<any>({ page: 1, page_size: 10 });
  const handlePagination: any = {
    pageSize: params.page_size,
    showSizeChanger: false,
    onChange: (current: number, size: number) => {
      setParams({ page: current, page_size: size });
    },
  };
  return (
    <ProTable
      columns={columns}
      scroll={{ y: 'calc(100vh - 300px)' }}
      request={(...tableParams) => {
        var filters: object = tableParams[2];
        for (let key in filters) {
          if (!filters[key]) {
            delete filters[key];
          }
        }
        return UserAll({ ...params, ...filters }, reqList);
      }}
      rowKey="id"
      pagination={handlePagination}
      size="middle"
      headerTitle={<h3 style={{ fontWeight: 'bold' }}>用户列表</h3>}
      search={false}
      toolBarRender={() => [
        <Button
          type="primary"
          icon={<PlusOutlined />}
          onClick={() => message.warning('待实现!')}
        >
          新建
        </Button>
      ]}
      dateFormatter="string"
    />
  );
};
export default React.memo(User);

保存文件,重启项目后访问项目地址并登录访问用户管理页,就可以看到下面的效果了:

五、增加token失效跳转登录页

这个我们可以在umi里RequestConfig的中间件层做,对于我们的接口规范(非2XX的接口返回的network状态就会是异常的状态码),可以直接在RequestConfigerrorConfig中来做, 将文件app.tsx中的errorConfig代码改为下面这样:

errorConfig: {
    adaptor: (res) => {
      var res_msg: string = res.msg;
      if (res.code === 401) {
        //代表token失效!
        res_msg = '该帐号在其他地方被登录!';
        history.push('/login');
      }
      return {
        success: res.success,
        data: res.data,
        errorCode: res.code,
        errorMessage: res_msg,
      };
    },
  },

这样token失效后就会给予提示并跳转到登录页:

六、总结

这一章涉及的内容比较多,比如django的中间件、DRF的分页等;前端的路由菜单和layout、protable的简单使用。需要小伙伴花些时间多阅读官方文档进行消化。当然结合实践更能够加深我们的印象。

当然这个过程中会存在不少疑问,可以点击下方在线演示地址进行反馈。

在线演示地址:http://121.43.43.59/ (帐号:admin 密码:123456)

欢迎在文章头部右上角订阅本专栏,及时获取最新教程分享!

相关文章