上一章节我们完成了前端基础框架的搭建以及结合接口完成了登录功能。这篇将实现用户的管理模块功能。
实现效果如下:
在线演示地址:http://121.43.43.59/ (帐号:admin 密码:123456)
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
创建完成的目录结构:
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姓名下的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 }
有些小伙伴会发现,我们之前写的用户接口是没有分页功能的,这里我们需要增加分页功能。
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的视图实现的查询列表接口就都有了分页的功能了。
增加按字段筛选功能,django-filter
提供了很方便的方法,两行代码就可以搞定:
1)安装django-filter
:
pip install django-filter
2)在user
视图类中加入下面的代码:
filter_backends = (DjangoFilterBackend,)
filter_fields = ('is_active',)
上述代码表示前端可以指定is_active
字段来进行筛选数据
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)在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来判断请求方法传递什么,来达到控制执行增删改查操作的目的。
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('/');
});
};
1)用户相关的接口是需要用户成功登录才能请求成功的,也就是验证请求header中的token
是否有效,所以我们需要在app.tsx
的request
中增加下面的代码:
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
用户列表使用了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);
保存文件,重启项目后访问项目地址并登录访问用户管理页,就可以看到下面的效果了:
这个我们可以在umi里RequestConfig
的中间件层做,对于我们的接口规范(非2XX的接口返回的network状态就会是异常的状态码),可以直接在RequestConfig
的errorConfig
中来做, 将文件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)
欢迎在文章头部右上角订阅本专栏,及时获取最新教程分享!
版权说明 : 本文为转载文章, 版权归原作者所有 版权申明
原文链接 : https://blog.csdn.net/momoda118/article/details/121801608
内容来源于网络,如有侵权,请联系作者删除!