01-前端万用模板

1. 初始化

本前端初始模板使用 Ant Design Pro

# 初始化命令
npm i @ant-design/pro-cli -g
pro create yuzi-generator-web-frontend
  1. 注意:一定要选择 umi@4!
YlVJa2tZblBQdk5TQVU2aGR2bHhTMkJ6OGU3SlJBYzBHL1NLdjlZQm1nPT0=
  1. 项目版本最好一致:6.0.0
QW9iZnBIelQvT2RGMzlhZTZGOGlWbUJ6OGU3SlJBYzBHL1NLdjlZQm1nPT0=
  1. 使用 npm, yarn, pnpm, cnpm 安装依赖,建议版本不要过低,node 版本在 16 及以上
TkpERzlicFErTVltVHl4T051Rkc1R0J6OGU3SlJBYzBHL1NLdjlZPQ==
  1. 运行 npm dev 测试执行;运行 npm start 可以开启 Mock 数据
S21aeVpqeGdheFdqaXpOZjNwYWxWV0J6OGU3SlJBYzBHL1NLdjlZPQ==

1. 初始化可能遇到的问题

  • 可能会因为缺少 .git 文件导致 husky 执行报错,忽略即可

2. 开发规范

  1. Prettier 格式化工具,用来统一代码格式
    • 格式化快捷键建议勾选,Vue 技术栈同学建议手动加 .Vue 后缀
T2RFZWV5WFZNRXVQd3hLWloxd0xDbUJ6OGU3SlJBYzBHL1NLdjlZPQ==
  1. Eslint 保持代码风格,减少代码出错
    • Vue 技术栈手动加上即可
bEdLN215RHJYaUthQjByNVloNEh5MkJ6OGU3SlJBYzBHL1NLdjlZQm1nPT0=

3. 模板瘦身

一次移除后,重启一下项目,看看能否正常运行,控制变量法,如果不行,直接回滚,一次性删除太多,容易找不到源头

1. 移除模块

1. 移除husky

一个用来提交前检查代码的规范,保证代码的一致性,一般用于团体协作,个人没有必要

TWE5OGlkUUErWlN0Yis5QTRseFFFR0J6OGU3SlJBYzBHL1NLdjlZQm1nPT0=

移除相关的命令

b1VodjIxYnZmL1F2K0x6M200Z3NKR0J6OGU3SlJBYzBHL1NLdjlZPQ==

2. 移除mock

mock 是官方提供的模拟数据,有真实的后端接口要对接

N0s3Y0ladU92OTIvYklMVHhneHpNR0J6OGU3SlJBYzBHL1NLdjlZPQ==
TU5WOUZCY0dFWTk3aXFzWEFHWkV6R0J6OGU3SlJBYzBHL1NLdjlZQm1nPT0=

3. 移除icons, manifest.json

图标和适配移动端所需要的 Json,直接删除即可

U3NxcUZUeVRCczZJOHdEd3FLU3NHV0J6OGU3SlJBYzBHL1NLdjlZPQ==

4. 移除cname

域名映射,官方提供的,和自己的域名无关

RWhGbWp3WWIyNmptcGR6b1hoaDd6V0J6OGU3SlJBYzBHL1NLdjlZPQ==

5. 移除国际化

一般项目只对本国用户去开放,而且访问人数也较少,没有必要去用国际化,会增加打包体积,而且页面加载速度也会变慢

  1. 前端本地执行:
yarn add eslint-config-prettier --dev

yarn add eslint-plugin-unicorn --dev
  1. node_modules/@umijs/lint/dist/config/eslint/index.js,注释掉 es2022 那行
RWh0N0pRY1paOElhVlhDMytMalhtV0J6OGU3SlJBYzBHL1NLdjlZQm1nPT0=
  1. 执行 i18n-remove 命令
S1RtcnFGSk9RaSt6OFh4TmxLSi9sMkJ6OGU3SlJBYzBHL1NLdjlZPQ==
  1. 删除 locales 文件夹
OUNIOGRXS1NDcnVXZHI5Q1Q4a3NsV0J6OGU3SlJBYzBHL1NLdjlZPQ==

6. 移除单元测试

Jest 相关的全部移除

TUkrdWtDM0dmSWhlRjc4MGRKQ0ZFbUJ6OGU3SlJBYzBHL1NLdjlZQm1nPT0=
ZUdqdHpWZXFwemZrV1FlVWtVRU03bUJ6OGU3SlJBYzBHL1NLdjlZPQ==

7. 移除types

自己会用 OpenAPI 规划去生成接口和类型

MGNKYlp0d05TOW9PaEZRVTRIUGI2V0J6OGU3SlJBYzBHL1NLdjlZPQ==

8. 移除swagger

dm9SSXhDVlJwcEQ1eW10R2YzMzIzMkJ6OGU3SlJBYzBHL1NLdjlZPQ==

9. 移除openapi.json

有自己的后端接口地址,不需要官方提供

elcrZ0F4WHJIZG5NeXkvekNFQTJvbUJ6OGU3SlJBYzBHL1NLdjlZPQ==

4. 基本类型

修改 typings.d.ts

cWNJN3B1emJHYklzTHFWYkVXWU0rR0J6OGU3SlJBYzBHL1NLdjlZQm1nPT0=
declare module 'slash2';
declare module '*.css';
declare module '*.less';
declare module '*.scss';
declare module '*.sass';
declare module '*.svg';
declare module '*.png';
declare module '*.jpg';
declare module '*.jpeg';
declare module '*.gif';
declare module '*.bmp';
declare module '*.tiff';
declare module 'omit.js';
declare module 'numeral';
declare module '@antv/data-set';
declare module 'mockjs';
declare module 'react-fittext';
declare module 'bizcharts-plugin-slider';

declare const REACT_APP_ENV: 'test' | 'dev' | 'pre' | false;

/**
 * 分页信息
 */
interface PageInfo<T> {
  current: number;
  size: number;
  total: number;
  records: T[];
}

/**
 * 分页请求
 */
interface PageRequest {
  current?: number;
  pageSize?: number;
  sortField?: string;
  sortOrder?: 'ascend' | 'descend';
}

/**
 * 删除请求
 */
interface DeleteRequest {
  id: number;
}

/**
 * 返回封装
 */
interface BaseResponse<T> {
  code: number;
  data: T;
  message?: string;
}

/**
 * 全局初始化状态
 */
interface InitialState {
  currentUser?: API.LoginUserVO;
}

5. 请求

1. 请求代码生成

注意:由于 Swagger V3 版本文件上传请求有 BUG,还是推荐用 Swagger 2 版本

openAPI: [
  {
    requestLibPath: "import { request } from '@umijs/max'",
    schemaPath: "http://localhost:8101/api/v2/api-docs",
    projectName: 'yuzi-generator-web-backend',
  },
],



 



ZnVDMXpsbUpENmlvQmpNZHU2eVRpR0J6OGU3SlJBYzBHL1NLdjlZPQ==

生成结果:

UFN3VGh3ZnYxd3FKUHBua0R4cldBMkJ6OGU3SlJBYzBHL1NLdjlZPQ==

2. 全局请求处理

  1. 修改 requestErrorConfig.ts 的名称为 requestConfig.ts
  2. 修改昵称至 requestConfig
ZXZsU01FSnBFT00zTDNXVVA4c2hQMkJ6OGU3SlJBYzBHL1NLdjlZQm1nPT0=
  1. app.tsx 中引入
MnJCQnF6aWk0a3NqWFJKWDRUWXBobUJ6OGU3SlJBYzBHL1NLdjlZPQ==
  1. 创建 index.ts 常量区别开发环境和部署环境
ZWU5WWZ3cHJmaUxVajIrVEhpcCtVR0J6OGU3SlJBYzBHL1NLdjlZPQ==
  1. 引入环境变量,根据环境变量区分不同环境请求地址
V1Q3WlJYdEhNSXk4ZDd5Vmt1MWp6R0J6OGU3SlJBYzBHL1NLdjlZQm1nPT0=
  1. 删除错误处理,官方给的过于复杂,这边直接删除
MkNjSUNuemlDZEZPMjJMQ09KUFY3R0J6OGU3SlJBYzBHL1NLdjlZPQ==
  1. 删除官方的拼接 Token,一般要使用 Token,可以直接在 Authorization 请求头中携带 Token
// 请求拦截器
requestInterceptors: [
  (config: RequestOptions) => {
    // 拦截请求配置,进行个性化处理。
    return config;
  },
],
ZXl0b3V0RzlKZnN3T2NKakxjNytUV0J6OGU3SlJBYzBHL1NLdjlZPQ==
  1. 修改封装好的响应拦截器
aFlKVUU0bDVxRkNlb21KYnRyaFowR0J6OGU3SlJBYzBHL1NLdjlZQm1nPT0=
// 响应拦截器
responseInterceptors: [
  (response) => {
    // 请求地址
    const requestPath: string = response.config.url ?? '';

    // 响应
    const { data } = response as unknown as ResponseStructure;
    if (!data) {
      throw new Error('服务异常');
    }

    // 错误码处理
    const code: number = data.code;
    // 未登录,且不为获取用户登录信息接口
    if (
      code === 40100 &&
      !requestPath.includes('user/get/login') &&
      !location.pathname.includes('/user/login')
    ) {
      // 跳转至登录页
      window.location.href = `/user/login?redirect=${window.location.href}`;
      throw new Error('请先登录');
    }

    if (code !== 0) {
      throw new Error(data.message ?? '服务器错误');
    }
    return response;
  },
],

6. 临时登录

  1. 修改 app.tsx 中的 getInitialState(),获取用户初始化状态,利用 Mock 模拟数据,替换后将报错的 username、avatar 等全局替换即可
/**
 * @see  https://umijs.org/zh-CN/plugins/plugin-initial-state
 * */
export async function getInitialState(): Promise<InitialState> {
  const initialState: InitialState = {
    currentUser: undefined,
  };

  // 如果不是登录页面,执行
  const { location } = history;
  if (!location.pathname.startsWith(loginPath)) {
    // 获取当前登录用户
    const res = await getLoginUser();
    initialState.currentUser = res.data;

    const mockUser: API.LoginUserVO = {
      userAvatar: 'https://gw.alipayobjects.com/zos/rmsportal/BiazfanxmamNRoxxVxka.png',
      userName: 'yupi',
      userRole: 'admin',
    };
    initialState.currentUser = mockUser;
  }
  return initialState;
}












 


 
 
 
 
 




TCtSNUptR2J4SlMvb2VOSEMxTVpDR0J6OGU3SlJBYzBHL1NLdjlZPQ==
  1. 先用 @ts-ignore 忽略 ts 类型提示错误
Y29PYmZMM3BUblpLMFQ0QjBkeG9oR0J6OGU3SlJBYzBHL1NLdjlZPQ==
a1ExbTVkdVZSQ2lnTVBSK2tvS21pbUJ6OGU3SlJBYzBHL1NLdjlZQm1nPT0=
R1F0LytGLzM4NW1jckdwWGFBaHNtbUJ6OGU3SlJBYzBHL1NLdjlZPQ==
N3RlU0hUYkVjMFVaU1JsTDloemFGbUJ6OGU3SlJBYzBHL1NLdjlZPQ==
  1. 注释请求后端的代码,访问主页面,不会再重定向到登录页
RjVBVG5iWU96ZEo0d2t0ZDR5cGtUMkJ6OGU3SlJBYzBHL1NLdjlZQm1nPT0=
  1. 如果侧边栏没有展示,给路由加上 name 属性即可
VjVnUkdrMWJUeW1PdVNyMUJqRXdYR0J6OGU3SlJBYzBHL1NLdjlZPQ==
  1. 查看登录效果
TmVDSndiWWVpTUh3ODYvTlVCbDdjMkJ6OGU3SlJBYzBHL1NLdjlZQm1nPT0=
  1. 优化 layout 的配置
// ProLayout 支持的api https://procomponents.ant.design/components/layout
// @ts-ignore
export const layout: RunTimeLayoutConfig = ({ initialState }) => {
  return {
    actionsRender: () => [<Question key="doc" />],
    avatarProps: {
      src: initialState?.currentUser?.userAvatar,
      title: <AvatarName />,
      render: (_, avatarChildren) => {
        return <AvatarDropdown>{avatarChildren}</AvatarDropdown>;
      },
    },
    waterMarkProps: {
      content: initialState?.currentUser?.userName,
    },
    footerRender: () => <Footer />,
    menuHeaderRender: undefined,
    // 自定义 403 页面
    // unAccessible: <div>unAccessible</div>,
    // 增加一个 loading 的状态

    ...defaultSettings
  };
};
OWZDUjFob0dSdXhUTWdEdERhUlZXbUJ6OGU3SlJBYzBHL1NLdjlZPQ==

7. 基础布局

1. 右上角信息

  1. 修改 app.tsx 的 layout
// ProLayout 支持的api https://procomponents.ant.design/components/layout
// @ts-ignore
export const layout: RunTimeLayoutConfig = ({ initialState }) => {
  return {
    avatarProps: {
      render:()=>{
        return <AvatarDropdown />
      }
    },
    waterMarkProps: {
      content: initialState?.currentUser?.userName,
    },
    footerRender: () => <Footer />,
    menuHeaderRender: undefined,
    // 自定义 403 页面
    // unAccessible: <div>unAccessible</div>,
    // 增加一个 loading 的状态

    ...defaultSettings
  };
};
YmtSaDRXUUZnRkRndSsxb0x4TXBibUJ6OGU3SlJBYzBHL1NLdjlZQm1nPT0=
  1. 修改 AvatarDropdown.tsx
import {userLogoutUsingPost} from '@/services/backend/userController';
import {LogoutOutlined, SettingOutlined, UserOutlined} from '@ant-design/icons';
import {history, useModel} from '@umijs/max';
import {Avatar, Button, Space} from 'antd';
import {stringify} from 'querystring';
import type {MenuInfo} from 'rc-menu/lib/interface';
import React, {useCallback} from 'react';
import {flushSync} from 'react-dom';
import {Link} from 'umi';
import HeaderDropdown from '../HeaderDropdown';

export type GlobalHeaderRightProps = {
  menu?: boolean;
  children?: React.ReactNode;
};

export const AvatarDropdown: React.FC<GlobalHeaderRightProps> = ({menu}) => {
  /**
   * 退出登录,并且将当前的 url 保存
   */
  const loginOut = async () => {
    await userLogoutUsingPost();
    const {search, pathname} = window.location;
    const urlParams = new URL(window.location.href).searchParams;
    /** 此方法会跳转到 redirect 参数所在的位置 */
    const redirect = urlParams.get('redirect');
    // Note: There may be security issues, please note
    if (window.location.pathname !== '/user/login' && !redirect) {
      history.replace({
        pathname: '/user/login',
        search: stringify({
          redirect: pathname + search,
        }),
      });
    }
  };

  const {initialState, setInitialState} = useModel('@@initialState');

  const onMenuClick = useCallback(
    (event: MenuInfo) => {
      const {key} = event;
      if (key === 'logout') {
        flushSync(() => {
          setInitialState((s) => ({...s, currentUser: undefined}));
        });
        loginOut();
        return;
      }
      history.push(`/account/${key}`);
    },
    [setInitialState],
  );

  const {currentUser} = initialState || {};

  // 未登录
  if (!currentUser) {
    return (
      <Link to="/user/login">
        <Button type="primary" shape="round">
          登录
        </Button>
      </Link>
    );
  }

  const menuItems = [
    ...(menu
        ? [
          {
            key: 'center',
            icon: <UserOutlined/>,
            label: '个人中心',
          },
          {
            key: 'settings',
            icon: <SettingOutlined/>,
            label: '个人设置',
          },
          {
            type: 'divider' as const,
          },
        ]
        : []),
    {
      key: 'logout',
      icon: <LogoutOutlined/>,
      label: '退出登录',
    },
  ];

  return (
    <HeaderDropdown
      menu={{
        selectedKeys: [],
        onClick: onMenuClick,
        items: menuItems,
      }}
      >
      <Space>
        {currentUser?.userAvatar ? (<Avatar size="small" src={currentUser?.userAvatar} />):
        (<Avatar size="small" icon={<UserOutlined/>}/>)}

        <span className="anticon">{currentUser?.userName ?? '无名'}</span>
      </Space>
    </HeaderDropdown>
  );
};
Si92NHJmOWVLcnZmenM3VEtVOVlDMkJ6OGU3SlJBYzBHL1NLdjlZPQ==

2. 底部信息

修改成自己的个人博客,GitHub等地址就可以了

import { GithubOutlined } from '@ant-design/icons';
import { DefaultFooter } from '@ant-design/pro-components';
import '@umijs/max';
import React from 'react';
const Footer: React.FC = () => {
  const defaultMessage = '程序员谷牛';
  const currentYear = new Date().getFullYear();
  return (
    <DefaultFooter
      copyright={`${currentYear} ${defaultMessage}`}
      links={[
        {
          key: 'codeNav',
          title: '个人主页',
          href: 'http://listao.cn',
          blankTarget: true,
        },
        {
          key: 'git',
          title: (
            <>
              <GithubOutlined /> listao源码
            </>
          ),
          href: 'https://gitee.com/listao',
          blankTarget: true,
        },
      ]}
      />
  );
};
export default Footer;
NDJvRXQyelNhbEhnaEpGUXlEREUyMkJ6OGU3SlJBYzBHL1NLdjlZQm1nPT0=

8. 权限管理

修改 access.ts,给管理员添加:access:canAdmin

/**
 * @see https://umijs.org/zh-CN/plugins/plugin-access
 * */
export default function access(initialState: InitialState) {
  const { currentUser } = initialState ?? {};
  return {
    canUser: currentUser,
    canAdmin: currentUser?.userRole === 'admin',
  };
}







 


R0FFZ3hvZ0UyQjloSyt3STF0bjEyMkJ6OGU3SlJBYzBHL1NLdjlZPQ==
YU5Ia21lVkkwUkpBUHRFWUZPeWpzV0J6OGU3SlJBYzBHL1NLdjlZQm1nPT0=

9. 用户登录

  1. 移除手机号登录,验证码错误也可以一并删除
ZWYvN1FVQTJjMGJCMTQ4cTVuWVNHbUJ6OGU3SlJBYzBHL1NLdjlZPQ==
  1. 移除自动登录,将忘记密码改为新用户注册
bTh0K2hYRmtZOWtrb2Vwck0ycC9ibUJ6OGU3SlJBYzBHL1NLdjlZQm1nPT0=
  1. 移除其他登录方式和一些无用的模块,清除不需要导入的模块和包,按 ctrl + alt + o
RitUNWNNTFh1UG83SExIaEE5VS9yMkJ6OGU3SlJBYzBHL1NLdjlZQm1nPT0=
  1. 调整新用户注册的位置,移除浮动,父标签加 textAlign: right
UFdCVTJ4VkNvZnoyZllYT05tNDh0MkJ6OGU3SlJBYzBHL1NLdjlZPQ==
  1. 修改 logo.svg,替换即可,将标题和副标题自己替换
eS9oQUlXakF1Mm04UlFaQU5YMWlLV0J6OGU3SlJBYzBHL1NLdjlZQm1nPT0=
  1. 移除错误提示,修改官方的用户名 usernamepassword 为 userAccount 和 userPassword
SStKTGVCdDRCZzJhUXpSNzZ4QWhNbUJ6OGU3SlJBYzBHL1NLdjlZPQ==
  1. 移除其他不必要的代码
aXZqN1REN09EZWU1c0lmQUN3elFqMkJ6OGU3SlJBYzBHL1NLdjlZQm1nPT0=
  1. 修改登录按钮后触发的事件
const handleSubmit = async (values: API.UserLoginRequest) => {
  try {
    // 登录
    const res = await userLogin({
      ...values,
    });
    if (res.code === 0) {
      const defaultLoginSuccessMessage = '登录成功!';
      message.success(defaultLoginSuccessMessage);
      // 保存已登录用户信息
      setInitialState({
        ...initialState,
        currentUser:res.data
      })
      const urlParams = new URL(window.location.href).searchParams;
      history.push(urlParams.get('redirect') || '/');
      return;
    }
    // 如果失败去设置用户错误信息
  } catch (error) {
    const defaultLoginFailureMessage = '登录失败,请重试!';
    console.log(error);
    message.error(defaultLoginFailureMessage);
  }
};



 
 
 




 
 
 
 











  1. 输入账号和密码,与后端数据库的一致即可,登录成功效果图
WmtKRjJJRnV3cE5nSG8wcUIyVzAyR0J6OGU3SlJBYzBHL1NLdjlZQm1nPT0=
  1. 之前由于初始化时,获取的是模拟状态。刷新后,用户状态会失效。将 app.tsx 的代码进行修改
export async function getInitialState(): Promise<InitialState> {
  const initialState: InitialState = {
    currentUser: undefined,
  };

  // 如果不是登录页面,执行
  const { location } = history;
  if (!location.pathname.startsWith(loginPath)) {
    // // 获取当前登录用户
    try{
      const res = await getLoginUser();
      initialState.currentUser = res.data;
    }catch (error:any){

    }
    // const mockUser: API.LoginUserVO = {
    //   userAvatar: 'https://gw.alipayobjects.com/zos/rmsportal/BiazfanxmamNRoxxVxka.png',
    //   userName: 'xiaobaitiao',
    //   userRole: 'admin',
    // };
    // initialState.currentUser = currentUser;
  }
  return initialState;
}










 
 



 
 
 
 
 
 



  1. 修改官方的退出登录接口,修改为自己后端的退出登录方法
  2. 给登录按钮添加跳转链接
bis4ZFZidUN5aDkwcE02VHFKTkkzbUJ6OGU3SlJBYzBHL1NLdjlZPQ==
  1. 根据后端的接口返回相对应的错误信息,再次修改 index.tsxhandleSubmit()
const handleSubmit = async (values: API.UserLoginRequest) => {
  try {
    // 登录
    const res = await userLogin({
      ...values,
    });
    const defaultLoginSuccessMessage = '登录成功!';
    message.success(defaultLoginSuccessMessage);
    // 保存已登录用户信息
    setInitialState({
      ...initialState,
      currentUser: res.data,
    });
    const urlParams = new URL(window.location.href).searchParams;
    history.push(urlParams.get('redirect') || '/');
    return;
    // 如果失败去设置用户错误信息
  } catch (error: any) {
    const defaultLoginFailureMessage = `登录失败,请重试!${error.message}`;
    message.error(defaultLoginFailureMessage);
  }
};

错误提示效果图:

RGJHQ2xPWEI1aG9NOXF2cWtiU3IxbUJ6OGU3SlJBYzBHL1NLdjlZPQ==
  1. 修改 ts 类型
YmVmZ1BsV0gzWUhjYmlzVmRzTTR2MkJ6OGU3SlJBYzBHL1NLdjlZQm1nPT0=

10. 用户管理

  1. 调整添加节点的代码
/**
 * 添加节点
 * @param fields
 */
const handleAdd = async (fields: API.RuleListItem) => {
  const hide = message.loading('正在添加');
  try {
    await addRule({
      ...fields,
    });
    hide();
    message.success('创建成功');
    return true;
  } catch (error:any) {
    hide();
    message.error('创建失败,'+error.message);
    return false;
  }
};










 
 
 
 
 
 
 


  1. 调整更新节点的方法
/**
 *  更新节点
 * @param fields
 */
const handleUpdate = async (fields: FormValueType) => {
  const hide = message.loading('正在更新');
  try {
    await updateRule({
      name: fields.name,
      desc: fields.desc,
      key: fields.key,
    });
    hide();
    message.success('更新成功');
    return true;
  } catch (error:any) {
    hide();
    message.error('更新失败,'+error.message);
    return false;
  }
};
  1. 调整删除节点的代码
/**
 * 删除节点
 * @param selectedRows
 */
const handleRemove = async (selectedRows: API.RuleListItem[]) => {
  const hide = message.loading('正在删除');
  if (!selectedRows) return true;
  try {
    await removeRule({
      key: selectedRows.map((row) => row.key),
    });
    hide();
    message.success('删除成功');
    return true;
  } catch (error:any) {
    hide();
    message.error('删除失败,'+error.message);
    return false;
  }
};
  1. 移除获取详情和批量删除的代码
YW9OK1BJTnpBZ29HRDNlRVhEbWtYR0J6OGU3SlJBYzBHL1NLdjlZQm1nPT0=
  1. 删除其余批量选择和显示详情的代码
dWtSQ0RmZXB2VklLcW90ejVMMWc1R0J6OGU3SlJBYzBHL1NLdjlZQm1nPT0=
L1hoRjZZeEdGeDVORUNEUEFOUGRvR0J6OGU3SlJBYzBHL1NLdjlZPQ==
OFdLSVpkTnNCNmRjUmdOYlk1aElFMkJ6OGU3SlJBYzBHL1NLdjlZQm1nPT0=
  1. 修改路由配置,将用户管理移动到管理员的二级路由,将原有的 TableList 文件夹下的 index.tsxcomponents 组件一并移动到 Admin/User 文件夹下
export default [
  {
    path: '/user',
    layout: false,
    routes: [{ name: '登录', path: '/user/login', component: './User/Login' }],
  },
  { path: '/welcome', name: '欢迎', icon: 'smile', component: './Welcome' },
  {
    path: '/admin',
    name: '管理页',
    icon: 'crown',
    access: 'canAdmin',
    routes: [
      { path: '/admin', redirect: '/admin/user'},
      { name: '用户管理', icon: 'table', path: '/admin/user', component: './Admin/User' },
    ],
  },

  { path: '/', redirect: '/welcome' },
  { path: '*', layout: false, component: './404' },
];












 
 
 
 





  1. 修改 ProTable 表格,请求自己的后台真实接口,修改 TS 类型
request={
  async (params, sort, filter)=>{
    const sortField = Object.keys(sort)?.[0];
    const sortOrder = sort?.[sortField]?? undefined;
    const {data,code} = await listUserByPage({
      ...params,
      sortField,
      sortOrder,
      ...filter
    } as API.UserQueryRequest);
    return {
      success:code===0,
      data:data?.records||[],
      total:Number(data?.total)||0,
    }
  }
}
MXFSWHVlTmFyRXRBenFEVGh2aS9kMkJ6OGU3SlJBYzBHL1NLdjlZPQ==
  1. 修改 columns 的配置,让前端的属性值和后端传递的对应即可,调整一下修改和删除的样式
const columns: ProColumns<API.RuleListItem>[] = [
  {
    title: 'id',
    dataIndex: 'id',
    valueType: 'text',
  },
  {
    title: '账号',
    dataIndex: 'userAccount',
    valueType: 'text',
  },
  {
    title: '用户名',
    dataIndex: 'userName',
    valueType: 'text',
  },
  {
    title: '头像',
    dataIndex: 'userAvatar',
    valueType: 'image',
    fieldProps: {
      width: 64,
    },
    hideInSearch: true,
  },
  {
    title: '简介',
    dataIndex: 'userProfile',
    valueType: 'textarea',
  },
  {
    title: '权限',
    dataIndex: 'userRole',
    valueEnum: {
      user: {
        text: '用户',
      },
      admin: {
        text: '管理员',
      },
    },
  },
  {
    title: '创建时间',
    sorter: true,
    dataIndex: 'createTime',
    valueType: 'dateTime',
    hideInSearch: true,
  },
  {
    title: '更新时间',
    sorter: true,
    dataIndex: 'updateTime',
    valueType: 'dateTime',
    hideInSearch: true,
  },
  {
    title: '操作',
    dataIndex: 'option',
    valueType: 'option',
    render: (_, record) => (
      <Space size={"middle"}>
        <Typography.Link
          key="config"
          onClick={() => {
            handleUpdateModalOpen(true);
            setCurrentRow(record);
          }}
          >
          修改
        </Typography.Link>
        <Typography.Link type="danger" key="subscribeAlert" href="@/pages/Admin/User/index">
          删除
        </Typography.Link>
      </Space>
    ),
  },
];

数据展示效果:

elM4UFJJRUc3R1RUL2JSUkNSZ0hiMkJ6OGU3SlJBYzBHL1NLdjlZQm1nPT0=
  1. 添加删除用户的功能
    • 先给链接添加触发事件,传入对应的数据行,方法名称修改一下,handleRemove -> handleDelete
WjhWNDlGWGpqS2NJMlZNd09IcFVnbUJ6OGU3SlJBYzBHL1NLdjlZPQ==

编写对应的删除代码,注意这边 handleDelete 从外面移到了里面,为了使用 actionRef 删除成功后自动页面加载

const handleDelete = async (row: API.User) => {
  const hide = message.loading('正在删除');
  if (!row) return true;
  try {
    await deleteUser({
      id: row.id,
    });
    hide();
    message.success('删除成功');
    actionRef?.current?.reload();
    return true;
  } catch (error: any) {
    hide();
    message.error('删除失败,' + error.message);
    return false;
  }
};
  1. 新增添加用户的功能。首先创建一个 createModal.tsx
aFlIdDcrTTFtNEJxR3ZMME9QSldSV0J6OGU3SlJBYzBHL1NLdjlZQm1nPT0=

编写代码,把 index.tsxhandleAdd 移动到此组件即可

import { addUser } from '@/services/backend/userController';
import { ProColumns, ProTable } from '@ant-design/pro-components';
import { message, Modal } from 'antd';
import React from 'react';

interface Props {
  modalVisible: boolean;
  columns: ProColumns<API.User>[];
  onSubmit: () => void;
  onCancel: () => void;
}

/**
 * 添加节点
 * @param fields
 */
const handleAdd = async (fields: API.UserAddRequest) => {
  const hide = message.loading('正在添加');
  try {
    await addUser({
      ...fields,
    });
    hide();
    message.success('创建成功');
    return true;
  } catch (error) {
    hide();
    message.error('创建失败');
    return false;
  }
};

/**
 * 创建数据弹窗
 * @param props
 * @constructor
 */
const CreateModal: React.FC<Props> = (props) => {
  const { columns, modalVisible, onCancel, onSubmit } = props;

  return (
    <Modal title={'新建'} open={modalVisible} destroyOnClose footer={null} onCancel={onCancel}>
      <ProTable<API.UserAddRequest>
        columns={columns}
        type="form"
        onSubmit={async (value) => {
      const success = await handleAdd(value);
      if (success) {
        onSubmit?.();
      }
    }}
        />
      </Modal>
      );
      };

      export default CreateModal;

编写是否显示的布尔值

// 是否显示新建窗口
const [createModalVisible, setCreateModalVisible] = useState<boolean>(false);
dFpSbERtWGorWWJ6S0tKeS9DalZSV0J6OGU3SlJBYzBHL1NLdjlZPQ==

添加 createModal 组件

<CreateModal
  modalVisible={createModalVisible}
  columns={columns}
  onSubmit={() => {
    setCreateModalVisible(false);
    actionRef.current?.reload();
  }}
  onCancel={() => {
    setCreateModalVisible(false);
  }}
  />
TkxldEZ4aHR3MWVIcFZyMndlS0VuV0J6OGU3SlJBYzBHL1NLdjlZQm1nPT0=

修改点击按钮的事件,让弹窗显示即可

UzR4TlRnNnE4MVM3WHU4UFJmVk5RbUJ6OGU3SlJBYzBHL1NLdjlZPQ==

利用 hideInForm 去除不必要显示的属性,eg:id、更新时间、创建时间

ZmEwL2hSYlFjY1RiWFNMZ1pERnF5V0J6OGU3SlJBYzBHL1NLdjlZQm1nPT0=

新建用户效果图

bk1kL2VUQ2pmWGlLUWVmeU81VnJ4MkJ6OGU3SlJBYzBHL1NLdjlZPQ==

用户简介由于后端的 UserAddRequest 没有 UserProfile 字段,因此不能新增,但用户可以自己修改,管理员不能去改用户简介

  1. 新增更新用户的功能,先将 index.tsxhandleUpdate() 剪切到 UpdateModal.tsx 然后做适当修改即可
MVBtWjNoVTY5WnpjU0Q3Z2VjVVZER0J6OGU3SlJBYzBHL1NLdjlZPQ==
import { updateUser } from '@/services/backend/userController';
import { ProColumns, ProTable } from '@ant-design/pro-components';
import { message, Modal } from 'antd';
import React from 'react';

interface Props {
  oldData?: API.User;
  modalVisible: boolean;
  columns: ProColumns<API.User>[];
  onSubmit: () => void;
  onCancel: () => void;
}

/**
 * 更新节点
 *
 * @param fields
 */
const handleUpdate = async (fields: API.UserUpdateRequest) => {
  const hide = message.loading('更新中');
  try {
    await updateUser(fields);
    hide();
    message.success('更新成功');
    return true;
  } catch (error: any) {
    hide();
    message.error('更新失败,' + error.message);
    return false;
  }
};

/**
 * 更新数据弹窗
 * @param props
 * @constructor
 */
const UpdateModal: React.FC<Props> = (props) => {
  const { oldData, columns, modalVisible, onCancel, onSubmit } = props;

  if (!oldData) {
    return <></>;
  }

  return (
    <Modal title={'更新'} open={modalVisible} destroyOnClose footer={null} onCancel={onCancel}>
      <ProTable<API.UserUpdateRequest>
        columns={columns}
        form={{
      initialValues: oldData,
    }}
        type="form"
        onSubmit={async (values) => {
      const success = await handleUpdate({
        ...values,
        id: oldData.id,
      });
      if (success) {
        onSubmit?.();
      }
    }}
        />
      </Modal>
      );
      };

      export default UpdateModal;
  1. 添加显示更新窗口的一个布尔值
// 是否显示更新窗口
const [updateModalVisible, setUpdateModalVisible] = useState<boolean>(false);
UEtJaVQ1cHhoNlRUWmpscTNrNUFEMkJ6OGU3SlJBYzBHL1NLdjlZQm1nPT0=
  1. 更新用户行 TS 类型
emFOU200b2xSWUNkc0RUdm1EVmQ5V0J6OGU3SlJBYzBHL1NLdjlZPQ==

修改对应的 Click 事件

ZCt6eWlHdGRGRTNtNlVha2RORHQrR0J6OGU3SlJBYzBHL1NLdjlZQm1nPT0=

添加组件

    <UpdateModal
        modalVisible={updateModalVisible}
        columns={columns}
        oldData={currentRow}
        onSubmit={() => {
          setUpdateModalVisible(false);
          setCurrentRow(undefined);
          actionRef.current?.reload();
        }}
        onCancel={() => {
          setUpdateModalVisible(false);
        }}
      />
UUU4aEpubWxiajhZRFpFamNMM1l3V0J6OGU3SlJBYzBHL1NLdjlZPQ==

更新效果图:

UlpUUXhUQW4vblViY1J4NmRqUFZDMkJ6OGU3SlJBYzBHL1NLdjlZPQ==

11. 其他

  1. 修改 title 和 logo
MU53ZHNLSGc4Mno1UXFJd29KWTNmR0J6OGU3SlJBYzBHL1NLdjlZQm1nPT0=
  1. 修改 footer 的背景色为 none
cFNyU3ZsMys0VEhaZFk3NlRLdVBWMkJ6OGU3SlJBYzBHL1NLdjlZPQ==
  1. BUG 修复:如果当前用户为管理员,路径为 admin/user,注销后再次登录,会发现无权限。将重定向取消后即可修复 BUG,这段代码在 AvatarDropdown.tsx
VGJUOWlTMWtlL0cvMGtVVVJWTklqV0J6OGU3SlJBYzBHL1NLdjlZPQ==

99. 集成

1. ReactPlayer

npm install react-player
import ReactPlayer from 'react-player';
import VideoList from "@/components/video/VideoList";

const videos: string[] = [
  'http://listao.cn:10000/i/0/2024/99/1073_1715009580.mp4',
  'http://listao.cn:20000/download/video/private/04_JinFan.mp4',
];

<div style={{marginTop: 20, border: '1px solid #ccc'}}>
<ReactPlayer
url={ videos }
controls
light
/>
</div>
<VideoList/>

 










 



1. VideoList

import React, { useState } from 'react';
import ReactPlayer from 'react-player';

const VideoList = () => {
  // 视频列表数据
  const videos = [
    'http://listao.cn:10000/i/0/2024/99/1073_1715009580.mp4',
    'http://listao.cn:10000/i/0/2024/99/001_shi_marriage.mp4',
    'http://listao.cn:10000/i/0/2024/99/003_dog.mp4',
  ];

  // 当前播放视频的索引
  const [currentIndex, setCurrentIndex] = useState(0);

  // 播放选中的视频
  const handlePlayVideo = (index: React.SetStateAction<number>) => {
    setCurrentIndex(index);
  };

  return (
    <div>
      {videos.map((video, index) => (
        // eslint-disable-next-line react/button-has-type
        <button key={index} onClick={() => handlePlayVideo(index)}>
          {`Play Video ${index + 1}`}
        </button>
      ))}
      <div>
        <ReactPlayer
          url={videos[currentIndex]}
          width="100%" // 宽度设置为100%
          height="500px" // 高度设置为500px
          controls // 显示控件
        />
      </div>
    </div>
  );
};

export default VideoList;

2. Build bug

fatal - Found conflicts in esbuild helpers: q (735.350caa53.async.js, reactPlayerFilePlayer.fa054de9.async.js), Y (735.350caa53.async.js, reactPlayerTwitch.2fb09500.async.js), k (735.350caa53.async.js, reactPlayerMixcloud.48e67d4d.async.js, reactPlayerSoundCloud.91e9166a.async.js), K (reactPlayerDailyMotion.cfc377d1.async.js, reactPlayerMixcloud.48e67d4d.async.js, reactPlayerPreview.5abe5c52.async.js, reactPlayerSoundCloud.91e9166a.async.js, reactPlayerVidyard.23c9140f.async.js), g (reactPlayerDailyMotion.cfc377d1.async.js, reactPlayerFacebook.6f913577.async.js, reactPlayerVidyard.23c9140f.async.js, reactPlayerWistia.4a1cb367.async.js, reactPlayerYouTube.b8f1d229.async.js), R (reactPlayerDailyMotion.cfc377d1.async.js, reactPlayerMux.f565c9c8.async.js, reactPlayerVimeo.f4d5eb6c.async.js, reactPlayerYouTube.b8f1d229.async.js), P (reactPlayerDailyMotion.cfc377d1.async.js, reactPlayerFacebook.6f913577.async.js, reactPlayerMixcloud.48e67d4d.async.js, reactPlayerPreview.5abe5c52.async.js, reactPlayerSoundCloud.91e9166a.async.js, reactPlayerVimeo.f4d5eb6c.async.js), v (reactPlayerDailyMotion.cfc377d1.async.js, reactPlayerFacebook.6f913577.async.js, reactPlayerFilePlayer.fa054de9.async.js, reactPlayerMixcloud.48e67d4d.async.js), _ (reactPlayerFilePlayer.fa054de9.async.js, reactPlayerSoundCloud.91e9166a.async.js, reactPlayerVimeo.f4d5eb6c.async.js), J (reactPlayerFilePlayer.fa054de9.async.js, reactPlayerYouTube.b8f1d229.async.js), A (reactPlayerFilePlayer.fa054de9.async.js, reactPlayerMixcloud.48e67d4d.async.js), N (reactPlayerMixcloud.48e67d4d.async.js, reactPlayerVidyard.23c9140f.async.js, reactPlayerWistia.4a1cb367.async.js), f (reactPlayerMixcloud.48e67d4d.async.js, reactPlayerTwitch.2fb09500.async.js, reactPlayerWistia.4a1cb367.async.js), m (reactPlayerMixcloud.48e67d4d.async.js, reactPlayerSoundCloud.91e9166a.async.js, reactPlayerTwitch.2fb09500.async.js, reactPlayerVidyard.23c9140f.async.js, reactPlayerVimeo.f4d5eb6c.async.js, reactPlayerWistia.4a1cb367.async.js), T (reactPlayerMux.f565c9c8.async.js, reactPlayerVidyard.23c9140f.async.js, reactPlayerVimeo.f4d5eb6c.async.js, reactPlayerWistia.4a1cb367.async.js), U (reactPlayerMux.f565c9c8.async.js, reactPlayerPreview.5abe5c52.async.js, reactPlayerSoundCloud.91e9166a.async.js, reactPlayerYouTube.b8f1d229.async.js), w (reactPlayerMux.f565c9c8.async.js, reactPlayerPreview.5abe5c52.async.js), L (reactPlayerPreview.5abe5c52.async.js, reactPlayerTwitch.2fb09500.async.js), V (reactPlayerSoundCloud.91e9166a.async.js, reactPlayerVimeo.f4d5eb6c.async.js), b (reactPlayerSoundCloud.91e9166a.async.js, reactPlayerVidyard.23c9140f.async.js), W (reactPlayerTwitch.2fb09500.async.js, reactPlayerWistia.4a1cb367.async.js, reactPlayerYouTube.b8f1d229.async.js)
info  - please set esbuildMinifyIIFE: true in your config file
fatal - [esbuildHelperChecker] Found conflicts in esbuild helpers.

 

export default defineConfig({
  /**
   * react-player 组件打包必须配置
   */
  esbuildMinifyIIFE: true,
}