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

news/2024/5/19 1:06:35 标签: ReactHooks, Antd, TS, XXL-JOB

目录

  • 一. 简述
  • 二. 模块规划
    • 2.1. 页面规划
    • 2.2. 模型实体定义
  • 三. 模块实现
    • 3.1. 用户分页搜索
    • 3.2. Modal 配置
    • 3.3. 创建用户表单
    • 3.4. 修改用户表单
    • 3.5. 删除
  • 四. 结束语

一. 简述

上一篇文章我们实现登录页面和管理页面的 Layout 骨架,并对接登录和登出接口。这篇文章我们将实现用户管理的模块和相应的接口。最后效果如下:
在这里插入图片描述

二. 模块规划

在开发之前我们需要对 xxl-job管理系统的用户模块进行规划。

2.1. 页面规划

一般我们都是从前端页面需要使用什么组件;后端接口需要哪些?
在这里插入图片描述
前端使用的组件:表格、分页、下拉框、输入框和按钮,就是一个很普通的 CRUD 管理页面,比较简单;接口也是围绕这些功能的:分页查询接口、创建用户接口、编辑用户接口和删除接口。

2.2. 模型实体定义

接着我们需要定义下前后端交互会使用的到的请求和响应实体的定义。

首先是用户分页查询接口的请求和响应:

// 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; // 权限
}

这里需要注意的是虽然我们表格中只有用户名和角色名两个显示属性,但是考虑到在编辑的时候需要根据角色显示权限信息,这里在分页查询中返回用户的权限数据。但是如果在一些复杂的分页表格中,不建议这样操作!

接着是用户创建和编辑的请求的定义:

// 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. 用户分页搜索

上面我们分析我们要使用组件,这里就不赘述了,直接上代码:

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 定义:

/**
 * 用户分页
 * @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中非常好用的地方。

// 加载用户列表
const loadUser = useRequest(UserApi.UserPage, {
  manual: true, // 手动调用
  onSuccess: ({records, total}) => { // 成功之后执行的操作
    setTotal(total);
    setDatasource(records);
  }
});

最后我们配合 useEffect使用,加载用户列表的接口会在加载用户管理页面的时候调用这个接口。

useEffect(() => {
  loadUser.run(pageQuery)
}, [])

这里我们介绍 ahooks 中的 useRequest这个工具 hooks

在这里插入图片描述
支持的功能很多,这里我们现使用这里的 loading 返回值。在我们请求接口的时候如果遇到网络抖动之类的加载缓慢的情况,让表格出现一个加载状态的图标是非常友好了,不然用户也很懵逼。在上面antd 提供了加载属性loading={loadUser.loading}我们只需要将这个值的变化交给 useRequest 就可以,完全不需要我们手动控制。

接下来我们实现上面搜索的功能。
在这里插入图片描述
这里一个是下拉框一个是输入框,我们直接使用的是 antd 的组件,我们仅需要实现清空输入和搜索两个按钮事件就可以了。对于清空搜索的点击事件,我们只需要将下拉选项设置为默认值,输入框清空就可以了,代码如下 :

// 清空搜索
const clearSearch = () => {
  setPageQuery({...pageQuery, role: -1, username: ""})
}

对于搜索我们仅需要手动调用分页接口就可以了,代码如下:

<Button 
  type='primary' 
  icon={<SearchOutlined />} 
  onClick={() => loadUser.run(pageQuery)}
>搜索</Button>

3.2. Modal 配置

这个用户管理的部分需要用到创建用户和编辑用户两个功能,在xxl-job中都是都通过打开弹窗进行操的,我们这里也是使用相同的逻辑。这里我们使用 antdModal 组件。在使用这个组件的时候我们需要对 Modal 组件的打开和关闭进行一个统一的控制。

// 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;  // 编辑是存放被编辑用户信息
}

接着我们分别定义打开和关闭模态框的事件:

// 关闭模态框
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
  }
}

最后我们在创建和编辑按钮上使用这些事件就可以了:

<Button 
  type="primary" 
  icon={<PlusOutlined />} 
  onClick={() => openModal('create')}
  >增加用户</Button>

<Button 
  type="primary" 
  onClick={() => openModal('update', record)}
  >编辑</Button>

接着我们定一个模态框组件,在当前目录下创建 create.tsxupdate.tsx 文件,这两个文件分别是创建用户和编辑用户模态框组件(子组件)。

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 中使用这个子组件就可以了。

// 存放模态框状态值
const [userModalProp, setUserModalProp] = useState<User.UserModalProp>({createVisible: false, updateVisible: false});

<CreateUserModal
  key="create"
  close={closeModal}  // 模态框关闭事件
  visible={userModalProp.createVisible} // 创建用户模态框打开状态标识变量
/>

效果如下:
在这里插入图片描述

3.3. 创建用户表单

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

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

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属性,当前模态框打开的时候,需要请求执行器列表,并设置角色默认值。

还需要注意的一个点是,当角色是管理员的时候,是不需要选择执行器的,所有在切换角色为管理员的时候,需要将之前选中的执行器清空;所以在最后提交用户数据的时候,设置下执行器就可以了。

const submitForm = () => {
  form.validateFields().then(value => {
    if (value.role == 1) { // 当角色是管理员的时候,将执行器权限设置为空数据
      value.permission = []
    }
    createLoader.run(value);
  })
}

此外我们还通过 styled 修改了 Modal 组件的样式,主要是为了将多选框flex 布局从 row 改为 column

// 使用 styled 包裹 Modal 组件
const Controller = styled(Modal)`
  .ant-modal-body {
    padding-top: 24px;
    .xxl-job-list {
      flex-direction: column;
    }
  }
`

3.4. 修改用户表单

有了上面创建用户表单部分,我们在修改用户信息的时候,仅需要了解表单初始化的问题了;这里我们也是用使用Form.setFieldsValue方法进行初始化表单,代码代码:

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

这里还有一个不一样的地方是我们会设置一个隐藏的用户主键,方便我们后面执行更新的时候确定要被更新用户信息:

<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或者气泡提示就可以了。

在这里插入图片描述
这里功能简单,只需要调用组件,在其回调方法中调用删除接口就可以了。代码如下:

// 移除用户
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 传给接口。

<Button type="link" danger onClick={() => deleteUser(record.id)}>删除</Button>

四. 结束语

这篇文章我们介绍了如何利用 antd 提供的组件,快速开发一个 CRUD 功能的管理模块,相信大家可以从中收获很多东西了;下一篇文章我们将介绍执行器管理的模块开发。


http://www.niftyadmin.cn/n/5356398.html

相关文章

Java-并发高频面试题

1.说一下你对Java内存模型&#xff08;JMM&#xff09;的理解&#xff1f; 其实java内存模型是一种抽象的模型&#xff0c;具体来看可以分为工作内存和主内存。 JMM规定所有的变量都会存储再主内存当中&#xff0c;再操作的时候需要从主内存中复制一份到本地内存&#xff08;c…

最全前端 HTML 面试知识点

一、HTML 1.1 HTML 1.1.1 定义 超文本标记语言&#xff08;英语&#xff1a;HyperTextMarkupLanguage&#xff0c;简称&#xff1a;HTML&#xff09;是一种用于创建网页的标准标记语言 HTML元素是构建网站的基石 标记语言&#xff08;markup language &#xff09; 由无数个…

基于单片机的烟草干燥温度控制系统设计

摘 要&#xff1a;烟草干燥研究一直备受国内外烟草工作者的重视&#xff0c;在烟草干燥的方法中热风管处理法是利用热空气对流使烟草达到干燥的效果&#xff0c;这样可以控制烟草干燥时的温度&#xff0c;使烟草能够更好更快地干燥&#xff0c;因此温度的检测和控制是很重要的。…

浏览器返回:1xx、2xx、3xx、40xx、50x代码含义,一文讲透.

浏览器返回状态代码&#xff08;Browser status codes&#xff09;是指在进行HTTP请求时&#xff0c;服务器返回给浏览器的状态码。这些状态码用于表示服务器对请求的处理结果&#xff0c;并且帮助浏览器和客户端了解请求的状态和是否成功。 1xx&#xff08;信息性状态代码&…

visual studio2022专业版安装步骤

目录 一、Visual studio下载二、创建C#项目——Hello World三、专业版秘钥激活 一、Visual studio下载 首先进入下载官网 先下载2022专业版&#xff0c;等等后面还需要选环境 我勾选了以下几个和c#开发有关的&#xff0c;后面缺什么还可以再安装所有以少勾了问题也不大 然后…

腾讯mini项目总结-指标监控服务重构

项目概述 本项目的背景是&#xff0c;当前企业内部使用的指标监控服务的方案的成本很高&#xff0c;无法符合用户的需求&#xff0c;于是需要调研并对比测试市面上比较热门的几款开源的监控方案&#xff08;选择了通用的OpenTelemetry协议&#xff1a;Signoz&#xff0c;otel-…

如何快速记忆小鹤双拼键位图?

记忆方法&#xff1a;韵母表 图形 最常用字 韵母表&#xff1a;双拼的基础 图形&#xff1a;帮助新手快速联想回忆 最常用字&#xff1a;快速打字基础 一、单韵母&#xff08;紫色方块&#xff09; 一一对应如下表&#xff1a; 单韵母aoeiu、AOEIV 二、复韵母—箭矢型&am…

Spring Boot JPA save之怪异

Spring Boot JPA save方法之怪异 项目场景问题描述问题原因解决方案搞定收工&#xff01; 项目场景 工作中的一个小需求&#xff0c;由于这个项目第一阶段只是引入&#xff0c;作为触发程序在后台跑数据&#xff0c;将下游的数据引入即可。所以不需要暴露在页面上。所以报错的话…