用React给XXL-JOB开发一个新皮肤(四):实现用户管理模块

目录

  • [一. 简述](#一. 简述)
  • [二. 模块规划](#二. 模块规划)
    • [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
}

三. 模块实现

从这个模块我们可以分为两个大部分和三个小组件组成。

其中功能部分我们可以使用 antdSpace 中嵌套表单组件实现;表格可以使用 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>

这个用户管理的部分需要用到创建用户和编辑用户两个功能,在xxl-job中都是都通过打开弹窗进行操的,我们这里也是使用相同的逻辑。这里我们使用 antdModal 组件。在使用这个组件的时候我们需要对 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.tsxupdate.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. 创建用户表单

这里我们接着实现创建用户表单和表单提交的相关部分,直接上代码:

推荐先看看 antdForm 组件的文章。

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 功能的管理模块,相信大家可以从中收获很多东西了;下一篇文章我们将介绍执行器管理的模块开发。

相关推荐
kp6i9HIY4G7WPzH14 天前
定时任务xxl-job使用改造使用postgresql数据库
数据库·spring boot·postgresql·中间件·xxl-job·定时任务·任务调度
__松子16 天前
TS项目中如何合理的为接口定义参数类型
前端·javascript·typescript·ts
深蓝浅蓝的天1 个月前
使用 Dockerfile 和启动脚本注册 XXL-Job 执行器的正确 IP 地址
docker·xxl-job
飞翔的佩奇2 个月前
xxl-job适配sqlite本地数据库及mysql数据库。可根据配置指定使用哪种数据库。
数据库·spring boot·mysql·sqlite·xxl-job·任务调度
offer来了2 个月前
分布式调度XXL-JOB
xxl-job
马剑威(威哥爱编程)2 个月前
xxl-job分布式任务调度平台
分布式·spring cloud·java-ee·xxl-job·威哥爱编程
优雅的大白鹅2 个月前
复杂的编辑表格
前端·javascript·react.js·antd
Evisu47(常安欧阳)3 个月前
Antd-React-TreeSelect前端搜索过滤
react·antd
S-X-S3 个月前
xxl-job定时任务同步点赞数据 + 内网穿透
网络·内网穿透·xxl-job
昔冰_G3 个月前
报红:找不到名称ref ts(2304)、‘ref‘ is not defined. eslint(no-undef)
前端·typescript·vue·vue3·ts·eslint·自动导入