NestJS实战-前后端联调

326 阅读6分钟

NestJS实战-前后端联调

本文介绍 NestJS 实战前后端联调的过程:前端请求封装、页面权限、按钮权限管理、页面的增删改查接口联调。

供自己以后查漏补缺,也欢迎同道朋友交流学习。

引言

之前几章已经介绍过 NestJS 实战的后端开发了,本章主要介绍前后端联调的一些过程,例如封装前端请求 JS、设置 proxy 解决接口跨域、路由权限配置、按钮权限判断、用户/文章/专栏 3 个模块功能实现。

前端开发的技术栈和安装不介绍了,可以看NestJS实战-前端搭建

接口请求封装

封装 request

src/utils 目录下新增 request.ts,使用 fetch 封装 handleRequest 请求处理、GETPOSTPATCHDELETE请求封装:

// src/utils/request.ts
import { message } from 'antd';
import { downloadBlob, transformBody, transformJsonToQuery } from './index';

const BASE_URL = '/api'; // 这个API基础URL可以根据环境进行配置

// 一个处理请求的函数,返回一个Promise
async function handleRequest<T>(url: string, options?: RequestInit): Promise<T> {
  try {
    // 请求全路径
    const fullUrl = `${BASE_URL}${url}`;
    // 写入token
    let headers = {
      'Content-Type': options?.headers?.['Content-Type'] || 'application/json',
      ...options.headers,
    };
    if (!fullUrl.includes('/auth/login')) {
      const token = localStorage.getItem('cms-project-token');
      headers.Authorization = `Bearer ${token}`;
    }

    const response = await fetch(fullUrl, {
      ...options,
      headers,
    });

    if (options?.responseType === 'blob') {
      const blobData = await response.blob();
      const contentDisposition = response.headers.get('Content-Disposition');
      console.log('@@@ contentDisposition', contentDisposition);
      if (blobData) {
        try {
          downloadBlob(blobData, contentDisposition);
          message.success('操作成功');
          return { message: '操作成功' };
        } catch {
          message.error('操作失败');
          return { message: '操作失败' };
        }
      }

      message.error('操作失败');
      return { message: '操作失败' };
    }

    const data = await response.json();

    if (!response.ok) {
      message.error(data?.msg || '服务调用失败');
      return null;
    }

    return data.result;
  } catch (error) {
    message.error('服务异常:', error);
    throw error;
  }
}

// GET请求
export async function get<T>(url: string, body: object): Promise<T> {
  // 过滤字符串 去除前后空格
  const query = transformBody(body);
  // 过滤query再转成字符串
  const queryStr = query ? transformJsonToQuery(query) : '';
  // 请求路由拼接
  const queryUrl = queryStr ? `${url}?${queryStr}` : url;

  return handleRequest<T>(queryUrl, {
    method: 'GET',
  });
}

// POST请求
export async function post<T>(url: string, body: object, config): Promise<T> {
  const filterBody = body ? transformBody(body) : undefined;
  return handleRequest<T>(url, {
    method: 'POST',
    body: JSON.stringify(filterBody),
    ...config,
  });
}

// PUT请求
export async function patch<T>(url: string, body: object): Promise<T> {
  const filterBody = body ? transformBody(body) : undefined;
  return handleRequest<T>(url, {
    method: 'PATCH',
    body: JSON.stringify(filterBody),
  });
}

// DELETE请求
export async function del<T>(url: string, body: object): Promise<T> {
  const filterBody = body ? transformBody(body) : undefined;
  return handleRequest<T>(url, {
    method: 'DELETE',
    body: JSON.stringify(filterBody),
  });
}

封装 utils

在 src/utils/index.ts 封装工具方法:

// 把json对象转换成str且过滤掉空值
export function transformJsonToQuery(json) {
  if (!json) return '';
  const params = new URLSearchParams();
  for (const [key, value] of Object.entries(json)) {
    if (value !== null && value !== undefined && value.toString().trim() !== '') {
      params.append(key, value);
    }
  }

  return params.toString();
}

// 把json对象、过滤undefined、过滤字符串 去除前后空格
export function transformBody(body) {
  if (!body) return null;

  let bodyParams = {};

  for (const [key, value] of Object.entries(body)) {
    // 过滤undefined
    let val = value === undefined ? null : value;
    // 过滤字符串 去除前后空格
    if (typeof value === 'string') {
      val = value.trim();
    }
    // null、空字符串、0、false都可以通过

    bodyParams[key] = val;
  }

  return bodyParams;
}

// 解析contentDisposition获取filename
export function parseContentDisposition(contentDisposition) {
  // 使用正则表达式匹配filename的值
  const matches = /filename[^;=\n]*=((['"]).*?\2|[^;\n]*)/.exec(contentDisposition);
  if (matches !== null && matches[1]) {
    // 解码文件名
    return decodeURIComponent(matches[1].replace(/['"]/g, ''));
  }
  return null;
}

// 下载Blob文件流
export function downloadBlob(blob, contentDisposition) {
  const url = window.URL.createObjectURL(blob);
  const a = document.createElement('a');
  a.style.display = 'none';
  a.href = url;
  if (contentDisposition) {
    a.download = parseContentDisposition(contentDisposition); // 设置文件名
  }
  document.body.appendChild(a);
  a.click();
  window.URL.revokeObjectURL(url);
  a.remove();
}

封装 业务请求库

src 下新建 service 目录,封装 user.ts 存放用户和权限接口:

// src/service/user.ts
import { get, post, del, patch } from "../utils/request";

// 登录
export const login = async (body) => {
  return await post('/auth/login', body)
}

// 当前用户
export const queryCurrentUser = async () => {
  return await get('/auth/queryCurrentUser')
}

// 帐号登出
export const logout = async () => {
  return await post('/auth/logout')
}

// 查询角色列表
export const queryRoleList = async () => {
  return await get('/user/roleList')
}

// 创建用户
export const createUser = async (body) => {
  return await post('/user', body)
}

// 删除用户
export const deleteUser = async (id) => {
  return await del(`/user/${id}`)
}

// 批量删除用户
export const batchDeleteUser = async (body) => {
  return await del('/user', body)
}

// 更新用户
export const updateUser = async (id, body) => {
  return await patch(`/user/${id}`, body)
}

// 查询用户
export const queryUser = async (id) => {
  return await get(`/user/${id}`)
}

// 查询用户列表
export const queryUserList = async (queryBody) => {
  return await get('/user', queryBody)
}

// 重置用户密码
export const resetPassword = async (body) => {
  return await post('/user/resetPassword', body)
}

// 用户修改密码
export const userUpdatePassword = async (body) => {
  return await post('/user/userUpdatePassword', body)
}

// 用户表勾选导出
export const userSelectExport = async (body) => {
  return await post('/user/userSelectExport', body, {
    responseType: 'blob'
  })
}

// 用户表查询导出
export const userSearchExport = async (body) => {
  return await post('/user/userSearchExport', body, {
    responseType: 'blob'
  })
}

article-column.ts 存放文章和专栏相关接口:

// src/service/article-column.ts
import { get, post, del, patch } from "../utils/request";

// 创建文章
export const createArticle = async (body) => {
  return await post('/article', body)
}

// 分页查询文章列表
export const queryArticleList = async (queryBody) => {
  return await get('/article', queryBody)
}

// 根据ID查询文章详情
export const queryArticleDetail = async (id) => {
  return await get(`/article/${id}`)
}

// 根据ID修改文章
export const updateArticle = async (id, body) => {
  return await patch(`/article/${id}`, body)
}

// 删除文章
export const deleteArticle = async (id) => {
  return await del(`/article/${id}`)
}

// 创建专栏
export const createColumn = async (body) => {
  return await post('/specialColumn', body)
}

// 专栏列表查询-不分页
export const queryColumnList = async () => {
  return await get('/specialColumn')
}

// 专栏列表下拉项
export const columnListForSelect = async () => {
  return await get('/specialColumn/columnListForSelect')
}

// 根据ID查询专栏
export const queryColumnDetail = async (id) => {
  return await get(`/specialColumn/${id}`)
}

// 根据ID修改专栏
export const updateColumnById = async (id, body) => {
  return await patch(`/specialColumn/${id}`, body)
}

// 删除专栏
export const deleteColumnById = async (id) => {
  return await del(`/specialColumn/${id}`)
}

// 根据专栏id查询文章列表
export const queryArticleListByColumnId = async (id, queryBody) => {
  return await get(`/specialColumn/queryArticleListByColumnId/${id}`, queryBody)
}

// 查询非该专栏的文章列表
export const queryOtherArticleListByColumnId = async (id, queryBody) => {
  return await get(`/specialColumn/queryOtherArticleListByColumnId/${id}`, queryBody)
}

// 批量取消收录
export const cancelArticleInclusion = async (body) => {
  return await post('/specialColumn/cancelArticleInclusion', body)
}

// 文章批量收录
export const addArticleInclusion = async (body) => {
  return await post('/specialColumn/addArticleInclusion', body)
}

接口代理 proxy

直接访问肯定会跨域,那需要设下代理,在 config/proxy.ts 文件下设置:

/**
 * @name 代理的配置
 * @see 在生产环境 代理是无法生效的,所以这里没有生产环境的配置
 * @doc https://umijs.org/docs/guides/proxy
 */
export default {
  /**
   * @name 详细的代理配置
   * @doc https://github.com/chimurai/http-proxy-middleware
   */
  dev: {
    // localhost:8000/api/** -> http://localhost:8004/**
    '/api/': {
      target: 'http://localhost:8004',
      changeOrigin: true,
      pathRewrite: {
        '^/api': '', // 重写路径:去掉路径中的 `/api`
      },
    },
  },
  test: {
    // localhost:8000/api/** -> http://localhost:8004/**
    '/api/': {
      target: 'http://localhost:8004',
      changeOrigin: true,
      pathRewrite: { '^/api': '' },  // 重写路径:去掉路径中的 `/api`
    },
  },
  pre: {
    // localhost:8000/api/** -> http://localhost:8004/**
    '/api/': {
      target: 'http://localhost:8004',
      changeOrigin: true,
      pathRewrite: { '^/api': '' },  // 重写路径:去掉路径中的 `/api`
    },
  },
};

因为我没用到 env 环境配置,其实生效的都是 dev 配置,如果要发布到线上还需要其他配置。

路由及页面级权限配置

页面权限配置

src/access.ts 中封装页面权限:

export default function access(initialState: { currentUser?: API.CurrentUser } | undefined) {
  const { currentUser } = initialState ?? {};
  return {
    canSystemAdmin: currentUser?.roleWeight === 0,
    canAdmin: currentUser?.roleWeight <= 1,
    canUser: currentUser?.roleWeight <= 2,
    canViewer: currentUser?.roleWeight === 3,
  };
}

路由配置

config/routes.ts 中封装路由,使用 access 权限控制:

export default [
  {
    name: 'login',
    path: '/login',
    layout: false,
    component: './Login',
  },
  {
    exact: true,
    path: '/welcome',
    name: 'welcome',
    icon: 'smile',
    component: './Welcome',
  },
  {
    exact: true,
    name: '账号管理',
    icon: 'user',
    path: '/account',
    component: './Account',
    access: 'canAdmin',
  },
  {
    exact: true,
    name: '账号管理',
    path: '/updatePassword',
    component: './UpdatePassword',
    hideInMenu: true,
  },
  {
    exact: true,
    name: '文章管理',
    icon: 'read',
    path: '/article',
    component: './Article/List',
  },
  {
    exact: true,
    name: '文章详情',
    path: '/article/detail',
    component: './Article/Detail',
    access: 'canUser',
    hideInMenu: true,
  },
  {
    exact: true,
    name: '文章详情',
    path: '/article/detail/:id',
    component: './Article/Detail',
    access: 'canUser',
    hideInMenu: true,
  },
  {
    exact: true,
    name: '专栏管理',
    path: '/column',
    icon: 'database',
    component: './Column/List',
  },
  {
    exact: true,
    name: '专栏详情',
    path: '/column/detail/:id',
    component: './Column/Detail',
    hideInMenu: true,
  },
  {
    path: '/',
    redirect: '/welcome',
  },
  {
    path: '*',
    layout: false,
    component: './404',
  },
];

主应用配置 app.tsx

app.tsx 主要配置 getInitialStatelayout

import type { Settings as LayoutSettings } from '@ant-design/pro-components';
import type { RunTimeLayoutConfig } from '@umijs/max';
import { history } from '@umijs/max';
import { App } from 'antd';
import defaultSettings from '../config/defaultSettings';
import { AvatarDropdown } from './components';
import { queryCurrentUser } from './service/user';
const loginPath = '/login';

export async function getInitialState(): Promise<{
  settings?: Partial<LayoutSettings>;
  currentUser?: object;
  loading?: boolean;
  fetchUserInfo?: () => Promise<object | undefined>;
}> {
  const fetchUserInfo = async () => {
    const user = await queryCurrentUser();

    if (!user) {
      history.push(loginPath);
      return null;
    }

    return user;
  };

  const { location } = history;
  if (location.pathname !== loginPath && location.pathname !== '/' ) {
    const currentUser = await fetchUserInfo();
    return {
      fetchUserInfo,
      currentUser,
      settings: defaultSettings as Partial<LayoutSettings>,
    };
  }
  return {
    fetchUserInfo,
    settings: defaultSettings as Partial<LayoutSettings>,
  };
}

export const layout: RunTimeLayoutConfig = ({ initialState }) => {
  return {
    avatarProps: {
      title: `${initialState?.currentUser?.username}-${initialState?.currentUser?.account}`,
      render: (_, avatarChildren) => {
        return <AvatarDropdown>{avatarChildren}</AvatarDropdown>;
      },
    },
    onPageChange: () => {
      const { location } = history;
      // 如果没有登录,重定向到 login
      if (!initialState?.currentUser && location.pathname !== loginPath) {
        history.push(loginPath);
      }
    },
    menuHeaderRender: undefined,
    // 自定义 403 页面
    // unAccessible: <div>unAccessible</div>,
    // 增加一个 loading 的状态
    childrenRender: (children) => {
      // if (initialState?.loading) return <PageLoading />;
      return (
        <App>
          <div>{children}</div>
        </App>
      );
    },
    ...initialState?.settings,
  };
};

功能模块

页面的功能联调我就仅展示下账号管理,其他可以去我的仓库地址里去看

文章管理

文章列表主页 src/pgaes/Account/index.tsx

// src/pgaes/Account/index.tsx
import {
  batchDeleteUser,
  createUser,
  queryUserList,
  resetPassword,
  updateUser,
  userSearchExport,
  userSelectExport,
} from '@/service/user';
import type { ActionType, ProColumns } from '@ant-design/pro-components';
import { PageContainer, ProTable } from '@ant-design/pro-components';
import { useModel } from '@umijs/max';
import { App, Button, message, Space } from 'antd';
import React, { useEffect, useRef, useState } from 'react';
import { defaultColumns } from './components/columns';
import RestPasswordModal from './components/RestPasswordModal';
import UserModal from './components/UserModal';

const AccountList: React.FC = () => {
  const { modal } = App.useApp();
  const actionRef = useRef<ActionType>();
  const { initialState } = useModel('@@initialState');
  const currentUser = initialState?.currentUser || {};

  const [selectedKeys, setSelectedKeys] = useState<API.RuleListItem[]>([]);
  const [columns, setColumns] = useState<ProColumns<API.RuleListItem>[]>([]);

  const [totalCount, setTotalCount] = useState<number>(0);
  const [queryParams, setQueryParams] = useState<object>(null);

  // 操作标识
  const [operateType, setOperateType] = useState('');
  const [userOpen, setUserOpen] = useState(false);
  const [userModalInfo, setUserModalInfo] = useState({});
  // 重置密码-操作标识
  const [restPasswordOpen, setRestPasswordOpen] = useState(false);
  const [restPasswordModalInfo, setRestPasswordModalInfo] = useState({});

  // 添加账号 - 弹窗
  const prevAddUser = () => {
    setOperateType('add');
    setUserModalInfo({});
    setUserOpen(true);
  };
  // 删除账号
  const deleteUser = (ids) => {
    modal.confirm({
      title: '删除提示',
      content: `确定删除id为${ids.join('、')}的账号吗`,
      onOk: async () => {
        const res = await batchDeleteUser({ ids });
        if (res) {
          message.success('删除成功');
          actionRef.current?.reloadAndRest();
          return;
        }
      },
    });
  };

  // 修改账号-弹窗
  const prevUpdateUser = (item) => {
    setOperateType('edit');
    setUserModalInfo(item);
    setUserOpen(true);
  };

  // 用户新建/修改
  const userOperateFinish = async (values) => {
    const { username, roleId } = values;

    let res = null;
    if (operateType === 'add') {
      res = await createUser(values);
    } else if (operateType === 'edit') {
      res = await updateUser(userModalInfo.id, { username, roleId });
    }

    if (res) {
      message.success('操作成功');
      setUserOpen(false);
      setUserModalInfo({});
      actionRef?.current?.reloadAndRest();
    }
  };

  // 重置密码-弹窗
  const prevRestPassword = (item) => {
    console.log(item);
    setRestPasswordModalInfo(item);
    setRestPasswordOpen(true);
  };

  // 重置密码-提交
  const restPasswordFinish = async (values) => {
    const { newPassword, confirmPassword } = values;

    if (newPassword !== confirmPassword) {
      message.error('确认密码与新密码必须一致,请修改');
      return;
    }

    const res = await resetPassword({ id: restPasswordModalInfo.id, newPassword, confirmPassword });
    if (res) {
      message.success('操作成功');
      setRestPasswordOpen(false);
      setRestPasswordModalInfo({});
    }
  };

  // 查询导出
  const searchExport = () => {
    if (!totalCount) {
      message.error('列表数据为空,无法查询导出');
      return;
    }

    userSearchExport(queryParams);
  };

  // 批量导出
  const selectExport = (selectedKeys) => {
    console.log('@@@ selectedKeys', selectedKeys);
    if (!selectedKeys?.length) {
      message.error('请勾选数据');
      return;
    }

    userSelectExport({ ids: selectedKeys });
  };

  useEffect(() => {
    // 访客不展示操作按钮
    if (currentUser.roleWeight === 3) {
      defaultColumns.pop();
    } else {
      defaultColumns[defaultColumns.length - 1].render = (text, item) => {
        // 当前权限权重要小于 该条权重才能就行操作 权重越小 权限越大
        return currentUser.roleWeight < item.roleWeight || currentUser.roleWeight === 0 ? (
          <>
            <Button type="link" onClick={() => prevUpdateUser(item)}>
              修改
            </Button>
            <Button type="link" onClick={() => deleteUser([item.id])}>
              删除
            </Button>
            <Button type="link" onClick={() => prevRestPassword(item)}>
              重置密码
            </Button>
          </>
        ) : null;
      };
    }

    setColumns(defaultColumns);
  }, []);

  // 表格查询
  const tableSearch = async (params) => {
    let values = {
      ...params,
      pageNum: params.current,
      pageSize: params.pageSize,
    };
    delete values.current;

    const res = await queryUserList(values);

    setQueryParams(values);
    setTotalCount(res?.total || 0);

    return { success: true, data: res?.list || [], total: res?.total || 0 };
  };

  return (
    <PageContainer>
      <ProTable
        actionRef={actionRef}
        rowKey={(item) => item.id}
        search={{
          labelWidth: 'auto',
          defaultCollapsed: false,
        }}
        request={tableSearch}
        columns={columns}
        toolBarRender={() =>
          currentUser.roleWeight === 3
            ? []
            : [
                <Button key="add" type="primary" onClick={prevAddUser}>
                  添加账号
                </Button>,
                <Button key="searchExport" type="primary" onClick={searchExport}>
                  查询导出
                </Button>,
              ]
        }
        tableAlertOptionRender={() =>
          currentUser.roleWeight === 3 ? null : (
            <Space>
              <Button key="delete" type="link" danger onClick={() => deleteUser(selectedKeys)}>
                批量删除
              </Button>
              <Button key="selectExport" type="link" onClick={() => selectExport(selectedKeys)}>
                批量导出
              </Button>
            </Space>
          )
        }
        scroll={{ x: 'max-content' }}
        rowSelection={
          currentUser.roleWeight === 3
            ? false
            : {
                onChange: (_) => {
                  setSelectedKeys(_);
                },
              }
        }
      />
      <UserModal
        modalInfo={userModalInfo}
        operateType={operateType}
        open={userOpen}
        onOpenChange={setUserOpen}
        onFinish={userOperateFinish}
      />
      <RestPasswordModal
        modalInfo={restPasswordModalInfo}
        open={restPasswordOpen}
        onOpenChange={setRestPasswordOpen}
        onFinish={restPasswordFinish}
      />
    </PageContainer>
  );
};

export default AccountList;

封装业务的列表字段值:

// src/pages/Account/components/columns.ts
import { queryRoleList } from '@/service/user';
import type { ProColumns } from '@ant-design/pro-components';

export const getRoleList = async () => {
  const list = await queryRoleList()

  return list.map(role => ({
    ...role,
    key: role.id,
    value: role.id,
    label: role.name,
  }))
}

export const defaultColumns: ProColumns<API.RuleListItem>[] = [
  {
    title: '账号ID',
    dataIndex: 'id',
    key: 'id',
    width: 70,
  },
  {
    title: '账号',
    dataIndex: 'account',
    width: 170,
  },
  {
    title: '用户名',
    dataIndex: 'username',
    width: 120,
  },
  {
    title: '权限',
    dataIndex: 'roleId',
    width: 110,
    valueType: 'select',
    request: async () => await getRoleList(),
    render: (text, item) => item?.roleName,
  },
  {
    title: '创建人',
    dataIndex: 'createdByAccount',
    search: false,
    width: 150,
  },
  {
    title: '创建时间',
    dataIndex: 'createdTime',
    search: false,
    width: 160,
  },
  {
    title: '更新人',
    dataIndex: 'updatedByAccount',
    search: false,
    width: 150,
  },
  {
    title: '更新时间',
    dataIndex: 'updatedTime',
    search: false,
    width: 160,
  },
  {
    title: '操作',
    dataIndex: 'operate',
    fixed: 'right',
    width: 230,
    search: false,
  },
];

新建/编辑用户弹窗:

// src/pages/Account/components/UserModal.tsx
import { ModalForm, ProFormSelect, ProFormText } from '@ant-design/pro-components';
import { getRoleList } from './columns';

const UserModal: React.FC = ({ modalInfo, operateType, open, onOpenChange, onFinish }) => {
  // 提交按钮
  const prevFinish = (values) => {
    onFinish(values);
  };

  if (!open) {
    return null;
  }

  return (
    <ModalForm
      layout="horizontal"
      width={480}
      title={`${operateType === 'add' ? '添加' : '修改'}用户账号`}
      open={open}
      onOpenChange={onOpenChange}
      modalProps={{
        destroyOnClose: true,
        onCancel: () => onOpenChange(false),
      }}
      onFinish={prevFinish}
    >
      <ProFormText
        name="account"
        label="账号"
        initialValue={modalInfo?.account}
        disabled={operateType === 'edit'}
        placeholder="请输入账号(邮箱格式)"
        rules={[
          { required: true, message: '请输入账号' },
          { type: 'email', message: '请输入账号邮箱格式)' },
        ]}
        fieldProps={{
          maxLength: 50,
        }}
      />
      <ProFormText
        name="username"
        label="用户名"
        initialValue={modalInfo?.username}
        rules={[{ required: true, message: '请输入用户名' }]}
        fieldProps={{
          maxLength: 50,
        }}
      />
      <ProFormSelect
        name="roleId"
        label="权限"
        initialValue={modalInfo?.roleId}
        rules={[{ required: true, message: '请选择权限' }]}
        request={getRoleList}
      />
      {operateType === 'add' ? (
        <ProFormText
          name="password"
          label="密码"
          width={240}
          fieldProps={{
            type: 'password',
            minLength: 8,
            maxLength: 16,
          }}
          placeholder="请输入8-16位数字+字母的密码"
          rules={[
            { required: true, message: '请输入密码' },
            {
              pattern: /^(?=.*[a-zA-Z])(?=.*\d).{8,16}$/,
              message: '请输入8-16位数字+字母的密码',
            },
          ]}
        />
      ) : null}
    </ModalForm>
  );
};

export default UserModal;

重置校验密码弹窗:

import { ModalForm, ProFormText } from '@ant-design/pro-components';

const RestPasswordModal: React.FC = ({ modalInfo, open, onOpenChange, onFinish }) => {
  // 提交按钮
  const prevFinish = (values) => {
    onFinish(values);
  };

  if (!open) {
    return null;
  }

  return (
    <ModalForm
      layout="horizontal"
      width={480}
      title="重置密码"
      open={open}
      onOpenChange={onOpenChange}
      modalProps={{
        destroyOnClose: true,
        onCancel: () => onOpenChange(false),
      }}
      onFinish={prevFinish}
    >
      <ProFormText
        name="account"
        label="账号"
        initialValue={modalInfo?.account}
        disabled
        placeholder="请输入邮箱格式的账号"
        rules={[
          { required: true, message: '请输入账号' },
          { type: 'email', message: '请输入邮箱格式的账号' },
        ]}
        fieldProps={{
          maxLength: 50,
        }}
      />
      <ProFormText
        name="newPassword"
        label="新密码"
        width={240}
        fieldProps={{
          type: 'password',
          minLength: 8,
          maxLength: 16,
        }}
        placeholder="请输入8-16位数字+字母的密码"
        rules={[
          { required: true, message: '请输入新密码' },
          {
            pattern: /^(?=.*[a-zA-Z])(?=.*\d).{8,16}$/,
            message: '请输入8-16位数字+字母的密码',
          },
        ]}
      />
      <ProFormText
        name="confirmPassword"
        label="确认密码"
        width={240}
        fieldProps={{
          type: 'password',
          minLength: 8,
          maxLength: 16,
        }}
        placeholder="确认密码与新密码一致"
        rules={[
          { required: true, message: '请输入确认密码' },
          {
            pattern: /^(?=.*[a-zA-Z])(?=.*\d).{8,16}$/,
            message: '请输入8-16位数字+字母的密码',
          },
        ]}
      />
    </ModalForm>
  );
};

export default RestPasswordModal;

文章管理和专栏关联

代码就不展示了,可以去代码库去看

实战合集地址

仓库地址