目录
- [一. 简述](#一. 简述)
- [二. 模块规划](#二. 模块规划)
- [2.1. 页面规划](#2.1. 页面规划)
- [2.2. 模型实体定义](#2.2. 模型实体定义)
- [三. 模块实现](#三. 模块实现)
- [3.1. 用户分页搜索](#3.1. 用户分页搜索)
- [3.2. Modal 配置](#3.2. Modal 配置)
- [3.3. 创建用户表单](#3.3. 创建用户表单)
- [3.4. 修改用户表单](#3.4. 修改用户表单)
- [3.5. 删除](#3.5. 删除)
- [四. 结束语](#四. 结束语)
一. 简述
上一篇文章我们实现登录页面和管理页面的 Layout
骨架,并对接登录和登出接口。这篇文章我们将实现用户管理的模块和相应的接口。最后效果如下:
二. 模块规划
在开发之前我们需要对 xxl-job
管理系统的用户模块进行规划。
2.1. 页面规划
一般我们都是从前端页面需要使用什么组件;后端接口需要哪些?
前端使用的组件:表格、分页、下拉框、输入框和按钮,就是一个很普通的 CRUD
管理页面,比较简单;接口也是围绕这些功能的:分页查询接口、创建用户接口、编辑用户接口和删除接口。
2.2. 模型实体定义
接着我们需要定义下前后端交互会使用的到的请求和响应实体的定义。
首先是用户分页查询接口的请求和响应:
typescript
// UserPageQueryProp 用户分页查询请求参数定义
export interface UserPageQueryProp {
page: number; // 页码
size: number; // 页大小
role: number; // 角色 ID
username?: string; // 用户名称
}
// UserTableProp 用户分页查询返回参数定义
export interface UserTableProp {
id: number; // 用户ID
username: string; // 用户名称
role: number; // 角色
permission: string; // 权限
}
这里需要注意的是虽然我们表格中只有用户名和角色名两个显示属性,但是考虑到在编辑的时候需要根据角色显示权限信息,这里在分页查询中返回用户的权限数据。但是如果在一些复杂的分页表格中,不建议这样操作!
接着是用户创建和编辑的请求的定义:
typescript
// UserTableProp 用户创建表单属性
export interface UserCreateFormProp {
username: string; // 用户名称
password: string; // 密码
role: number; // 角色
permission: string[]; // 权限
}
// UserUpdateFormProp 用户创建表单属性
export interface UserUpdateFormProp extends UserCreateFormProp{
id: number; // 用户ID
}
三. 模块实现
从这个模块我们可以分为两个大部分和三个小组件组成。
其中功能部分我们可以使用 antd
的Space
中嵌套表单组件实现;表格可以使用 Table
组件(这个组件自带分页功能)实现;最后创建和编辑按钮我们使用 Modal
组件中嵌套 Form
表单组件实现就可以了。下面我们按功能一个个实现这个用户模块。
这里我们在使用
TS
这个Buff
的使用,大部分使用需要申明类型,尤其在使用不熟悉的UI
组件库的时候,大家需要多读文章,多看组件定义文件或者源码。
3.1. 用户分页搜索
上面我们分析我们要使用组件,这里就不赘述了,直接上代码:
ts
import {Button, Divider, Input, Select, Space, Table, Tag} from "antd";
import React, {useEffect, useState} from "react";
import {User} from "@/types";
import {ColumnsType} from "antd/es/table";
import {useRequest} from "ahooks";
import UserApi from "@/api/user.ts";
import {ClearOutlined, PlusOutlined, SearchOutlined} from "@ant-design/icons";
const UserPage = () => {
// 定义列信息
const columns: ColumnsType<User.UserTableProp> = [
{
title: '账号',
key: 'username',
dataIndex: 'username',
align: 'center'
},
{
title: '角色',
key: 'role',
dataIndex: 'role',
align: 'center',
render: (_, record) => record.role == 1 ? <Tag color="#f50">管理员</Tag> : <Tag color="#2db7f5">普通用户</Tag>
},
{
title: '操作',
key: 'active',
align: 'center',
width: 200,
render: (_, record) => <Space>
<Button type="primary" onClick={() => openEdit(record.id)}>编辑</Button>
<Button type="primary" danger onClick={() => deleteUser(record.id)}>删除</Button>
</Space>,
},
]
// 总条数
const [total, setTotal] = useState<number>(0);
const [selectedRowKeys, setSelectedRowKeys] = useState<number[]>([]);
// 用户数据
const [datasource, setDatasource] = useState<User.UserTableProp[]>([]);
// 分页查询属性
const [pageQuery, setPageQuery] = useState<User.UserPageQueryProp>(defaultUserPageQuery());
return <div>
<Space>
<Button type="primary" icon={<PlusOutlined />}>增加用户</Button>
<Divider type="vertical"/>
<div>角色:</div>
<Select
onChange={e => setPageQuery({...pageQuery, role: e})}
placeholder="选择状态"
defaultValue={-1}
style={{width: 100}}
options={[
{value: -1, label: '全部'},
{value: 1, label: '管理员'},
{value: 0, label: '普通用户'}
]}
/>
<div style={{marginLeft: 20}}>用户名称:</div>
<Input
allowClear
placeholder="请输入搜索的用户名称"
value={pageQuery.username}
onChange={e => setPageQuery({...pageQuery, username: e.target.value})} />
<Button danger type='primary' icon={<ClearOutlined />} onClick={clearSearch}>清空</Button>
<Button type='primary' icon={<SearchOutlined />} onClick={() => loadUser.run(pageQuery)}>搜索</Button>
</Space>
<Table
bordered
size={'small'}
columns={columns}
loading={loadUser.loading}
dataSource={datasource}
style={{ marginTop: 10 }}
rowKey={(record) => record.id}
pagination={{
onShowSizeChange: (current, size) => loadUser.run({...pageQuery, page: current, size: size}),
onChange: (page, pageSize) => loadUser.run({...pageQuery, page: page, size: pageSize}),
showTotal: () => `共 ${total} 个`,
showQuickJumper: true,
showSizeChanger: true,
pageSize: pageQuery.size,
current: pageQuery.page,
size: 'default',
total: total,
}}
rowSelection={{
type: 'checkbox',
selectedRowKeys: selectedRowKeys,
onChange: (selectedRowKeys: React.Key[]) => {
setSelectedRowKeys([...selectedRowKeys.map(item => item as number)])
}
}}
/>
</div>
}
export default UserPage;
这里我们需要注意一下几点:
- 表格每一个行都需要一个
Key
,默认是React.Key
,但是如果我们需要自定义的时候,可以使用rowKey={(record) => record.id}
定义自己的rowKey
,这里的record
就是定义表格属性模型:User.UserTableProp
- 关于分页属性我们可以通过
pagination
属性进行设置,可以设置属性和方法可以在分页组件文章中看到 - 最后一点就是关于表格行选中可以通过
rowSelection
属性设置;
接下来我们就需要对接分页查询的接口了,首先我们在 api/user.ts
中添加用户分页接口 api
定义:
ts
/**
* 用户分页
* @param param
* @constructor
*/
export const UserPage = (param: User.UserPageQueryProp): Promise<PageData<User.UserTableProp>> => {
return https.request({
url: '/user/pageList',
method: 'post',
data: param
})
}
接着我们看一下如何使用这个api
,并且了解下ahooks
中的useRequest
中非常好用的地方。
ts
// 加载用户列表
const loadUser = useRequest(UserApi.UserPage, {
manual: true, // 手动调用
onSuccess: ({records, total}) => { // 成功之后执行的操作
setTotal(total);
setDatasource(records);
}
});
最后我们配合 useEffect
使用,加载用户列表的接口会在加载用户管理页面的时候调用这个接口。
ts
useEffect(() => {
loadUser.run(pageQuery)
}, [])
这里我们介绍 ahooks
中的 useRequest
这个工具 hooks
。
支持的功能很多,这里我们现使用这里的 loading
返回值。在我们请求接口的时候如果遇到网络抖动之类的加载缓慢的情况,让表格出现一个加载状态的图标是非常友好了,不然用户也很懵逼。在上面antd
提供了加载属性loading={loadUser.loading}
我们只需要将这个值的变化交给 useRequest
就可以,完全不需要我们手动控制。
接下来我们实现上面搜索的功能。
这里一个是下拉框一个是输入框,我们直接使用的是 antd
的组件,我们仅需要实现清空输入和搜索两个按钮事件就可以了。对于清空搜索的点击事件,我们只需要将下拉选项设置为默认值,输入框清空就可以了,代码如下 :
ts
// 清空搜索
const clearSearch = () => {
setPageQuery({...pageQuery, role: -1, username: ""})
}
对于搜索我们仅需要手动调用分页接口就可以了,代码如下:
ts
<Button
type='primary'
icon={<SearchOutlined />}
onClick={() => loadUser.run(pageQuery)}
>搜索</Button>
3.2. Modal 配置
这个用户管理的部分需要用到创建用户和编辑用户两个功能,在xxl-job
中都是都通过打开弹窗进行操的,我们这里也是使用相同的逻辑。这里我们使用 antd
的 Modal
组件。在使用这个组件的时候我们需要对 Modal
组件的打开和关闭进行一个统一的控制。
ts
// UserCreateModelProp 创建用户弹窗属性
export interface UserCreateModalProp {
visible: boolean;
close: (isLoad: boolean) => void; // 关闭模态框
}
// UserUpdateFormProp 用户更新表单属性
export interface UserUpdateFormProp {
id: number; // 用户ID
password: string; // 密码
role: number; // 角色
permission: string[]; // 权限
}
// 定义模态框的类型
export type ModalType = "create" | "update";
// UserModelProp 用户模态框汇总属性
export interface UserModalProp {
createVisible: boolean; // 创建用户模态框打开标识
updateVisible: boolean; // 编辑用户模态框打开标识
userData?: UserTableProp; // 编辑是存放被编辑用户信息
}
接着我们分别定义打开和关闭模态框的事件:
ts
// 关闭模态框
const closeModal = (isLoad: boolean) => {
// 在全局只能有一个弹窗打开,所以在关闭的时候把标识变量都设为 false 就可以了
setUserModelProp({createVisible: false, updateVisible: false, userData: undefined})
if(isLoad) {
// 如果创建和编辑成功,我们需要重新加载表格数据显示最新的数据
loadUser.run(pageQuery)
}
}
// 打开模态框
const openModal = (types: User.ModalType, data?: User.UserTableProp) => {
switch (types) {
case "create":
setUserModalProp({createVisible: true, updateVisible: false});
break;
case "update":
setUserModalProp({updateVisible: true, createVisible: false, userData: data});
break;
default:
break
}
}
最后我们在创建和编辑按钮上使用这些事件就可以了:
ts
<Button
type="primary"
icon={<PlusOutlined />}
onClick={() => openModal('create')}
>增加用户</Button>
<Button
type="primary"
onClick={() => openModal('update', record)}
>编辑</Button>
接着我们定一个模态框组件,在当前目录下创建 create.tsx
和 update.tsx
文件,这两个文件分别是创建用户和编辑用户模态框组件(子组件)。
ts
import {Modal} from "antd";
import React from "react";
import {User} from "@/types";
const CreateUserModal: React.FC<User.UserCreateModalProp> = ({visible, close}) => {
const submitForm = () => {
close(true)
}
return <Modal
title="创建用户"
open={visible}
onOk={submitForm}
onCancel={() => close(false)}
>
<h1>创建用户</h1>
</Modal>
}
export default CreateUserModal;
编辑类似不做展示了
这两个子组件设置组件之间的传值问题,我们在User.UserCreateModalProp
定义了创建用户模态框组件需要的参数:visible
变量和 close
函数。最后我们在 index.tsx
中使用这个子组件就可以了。
ts
// 存放模态框状态值
const [userModalProp, setUserModalProp] = useState<User.UserModalProp>({createVisible: false, updateVisible: false});
<CreateUserModal
key="create"
close={closeModal} // 模态框关闭事件
visible={userModalProp.createVisible} // 创建用户模态框打开状态标识变量
/>
效果如下:
3.3. 创建用户表单
这里我们接着实现创建用户表单和表单提交的相关部分,直接上代码:
推荐先看看
antd
的Form
组件的文章。
ts
import {Checkbox, Divider, Empty, Form, Input, message, Modal, Radio, Row, Spin, Tag} from "antd";
import React, {useEffect, useState} from "react";
import {Group, User} from "@/types";
import {useRequest} from "ahooks";
import {GroupApi, UserApi} from "@/api/index.ts";
import styled from "@emotion/styled";
const CreateUserModal: React.FC<User.UserCreateModalProp> = ({visible, close}) => {
// 表单
const [form] = Form.useForm<User.UserCreateFormProp>();
// 监听表单 role 的 value
const roleValue = Form.useWatch('role', form);
// 执行器列表
const [groups, setGroups] = useState<Group.JobGroupListProp[]>([]);
// 执行器请求
const groupLoader = useRequest(GroupApi.GroupLists, {manual: true, onSuccess: (data) => {
setGroups(data);
}})
// 创建用户请求
const createLoader = useRequest(UserApi.CreateUser, {manual: true, onSuccess: () => {
message.success('创建用户成功')
close(true)
}
});
const submitForm = () => {
form.validateFields().then(value => {
// console.log("submit => ", value)
if (value.role == 1) {
value.permission = []
}
createLoader.run(value);
})
}
// 监听 visible 打开关闭标识
useEffect(() => {
if (visible) { // 当创建用户模态框打开,请求执行器列表接口并设置角色默认值为普通用户
groupLoader.run();
form.setFieldValue('role', 0)
} else {
// 关闭模态框的时候,将表单置为空并将执行器列表设置为空数组
form.resetFields();
setGroups([]);
}
}, [visible])
return <Controller
title="创建用户"
maskClosable
width={500}
open={visible}
onOk={submitForm}
onCancel={() => close(false)}
>
<Spin tip="加载中......" spinning={createLoader.loading}>
<Form
form={form}
layout="vertical"
name="form_create_modal">
<Form.Item
name="username"
label="账号"
rules={[{ required: true, message: '请输入账号' }]}>
<Input placeholder="请输入账号" />
</Form.Item>
<Form.Item
name="password"
label="密码"
rules={[{ required: true, message: '请输入密码' }]}>
<Input.Password placeholder="请输入密码" />
</Form.Item>
<Form.Item name="role" label="角色">
<Radio.Group>
<Radio value={0}>普通用户</Radio>
<Radio value={1}>管理员</Radio>
</Radio.Group>
</Form.Item>
{
roleValue === 0 && <Form.Item name="permission" label="权限">
{groups.length > 0 ? <Checkbox.Group className="xxl-job-list">
{groups.map(item =>
<Row key={item.id}>
<Checkbox value={item.appName}>{item.title}
<Divider type="vertical" />
<Tag color="lime">{item.appName}</Tag>
</Checkbox>
</Row>)}
</Checkbox.Group>
: <Empty />
}
</Form.Item>
}
</Form>
</Spin>
</Controller>
}
const Controller = styled(Modal)`
.ant-modal-body {
padding-top: 24px;
.xxl-job-list {
flex-direction: column;
}
}
`
export default CreateUserModal;
这里我们通过 Modal
包裹表单组件,使用 useEffect
监听 visible
属性,当前模态框打开的时候,需要请求执行器列表,并设置角色默认值。
还需要注意的一个点是,当角色是管理员的时候,是不需要选择执行器的,所有在切换角色为管理员的时候,需要将之前选中的执行器清空;所以在最后提交用户数据的时候,设置下执行器就可以了。
ts
const submitForm = () => {
form.validateFields().then(value => {
if (value.role == 1) { // 当角色是管理员的时候,将执行器权限设置为空数据
value.permission = []
}
createLoader.run(value);
})
}
此外我们还通过 styled
修改了 Modal
组件的样式,主要是为了将多选框flex
布局从 row
改为 column
。
typescript
// 使用 styled 包裹 Modal 组件
const Controller = styled(Modal)`
.ant-modal-body {
padding-top: 24px;
.xxl-job-list {
flex-direction: column;
}
}
`
3.4. 修改用户表单
有了上面创建用户表单部分,我们在修改用户信息的时候,仅需要了解表单初始化的问题了;这里我们也是用使用Form.setFieldsValue
方法进行初始化表单,代码代码:
ts
useEffect(() => {
if (visible && data) {
groupLoader.run();
form.setFieldsValue({id: data.id, username: data.username, role: data.role, permission: data.permission})
} else {
form.resetFields();
setGroups([]);
}
}, [visible])
这里还有一个不一样的地方是我们会设置一个隐藏的用户主键,方便我们后面执行更新的时候确定要被更新用户信息:
typescript
<Controller
title="更新用户"
maskClosable
width={500}
open={visible}
onOk={submitForm}
onCancel={() => close(false)}
>
<Spin tip="加载中......" spinning={updateLoader.loading}>
<Form form={form} layout="vertical" name="form_update_modal">
// 不显示主键,在我们提交数据的时候会反给form.validateFields().then(value => {})中
<Form.Item name="id" label="主键" style={{display: 'none'}}><Input /></Form.Item>
<Form.Item name="username" label="账号">
<Input placeholder="请输入账号" readOnly />
</Form.Item>
<Form.Item name="password" label="密码">
<Input.Password placeholder="请输入新密码,为空则不更新密码" />
</Form.Item>
<Form.Item name="role" label="角色">
<Radio.Group>
<Radio value={0}>普通用户</Radio>
<Radio value={1}>管理员</Radio>
</Radio.Group>
</Form.Item>
{
roleValue === 0 && <Form.Item name="permission" label="权限">
{groups.length > 0 ? <Checkbox.Group className="xxl-job-list">
{groups.map(item =>
<Row key={item.id}><Checkbox value={item.appName}>{item.title}<Divider type="vertical" /><Tag color="lime">{item.appName}</Tag></Checkbox></Row>)}
</Checkbox.Group> : <Empty />}
</Form.Item>
}
</Form>
</Spin>
</Controller>
3.5. 删除
终于快要搞完了,现在我们就剩删除用户这个功能了。针对我们删除来说,一般我都需要弹出一个提示,询问用户是否确定删除这条数据。这里我们可以使用 antd
中的 删除Modal
或者气泡提示就可以了。
这里功能简单,只需要调用组件,在其回调方法中调用删除接口就可以了。代码如下:
ts
// 移除用户
const loadRemoveUser = useRequest(UserApi.RemoveUser, {
manual: true,
onSuccess: () => {
loadUser.run(pageQuery);
message.success('移除用户成功');
}
})
// 删除用户
const deleteUser = (id: number) => {
Modal.confirm({
title: '你确认删除当前用户吗?',
icon: <ExclamationCircleFilled />,
content: '删除用户会导致无法登录和操作任务',
okText: '确认',
okType: 'danger',
cancelText: '取消',
onOk() {
loadRemoveUser.run(id)
},
onCancel() {},
});
}
最后在给删除按钮添加点击事件,并将用户的 ID
传给接口。
ts
<Button type="link" danger onClick={() => deleteUser(record.id)}>删除</Button>
四. 结束语
这篇文章我们介绍了如何利用 antd
提供的组件,快速开发一个 CRUD
功能的管理模块,相信大家可以从中收获很多东西了;下一篇文章我们将介绍执行器管理的模块开发。