跳到主要内容

Ant Design Pro

初始化时依赖包报错

配置 tsconfig.json:

{
"compilerOptions": {
"forceConsistentCasingInFileNames": false
}
}

添加路由

config/routes.ts下配置,比如添加 home 路由,先在src/pages下新建 Home.tsx 路由组件

export default [
{
path: '/home',
name: 'home',
icon: 'home',
component: './Home'
}
];

CSS 方案

CSS Modules 模块化

  • 目的:避免样式冲突
  • 原理:对每个类名按照一定规则进行转换
import styles from './example.less';
export default ({ title }) => <div className={styles.title}>{title}</div>;

如果在浏览器查看 Dom 结构,可以看到类名被自动添加了一个 hash 值,保证了它的唯一性

<div class="title___3TqAx">title</div>

如果想让样式全局生效可以使用 :global

example.less
.title {
margin-bottom: 16px;
color: @heading-color;
font-weight: 600;
}

/* 定义全局样式 */
:global(.text) {
font-size: 16px;
}

/* 定义多个全局样式 */
:global {
.footer {
color: #ccc;
}
.sider {
background: #ebebeb;
}
}

CSS-in-JS

数据流方案

全局初始数据

文档

src/app.tsx里初始化全局数据,调用时initialState就是全局数据

import { useModel } from '@umijs/max';

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

简易数据流

文档

src/models 目录下新建文件

// demo.ts
export default () => 'Hello World';

使用:

import { useModel } from 'umi';

export default () => {
const message = useModel('demo');
return <div>{message}</div>;
};

dva

model

持久化缓存:redux-persist

新建src/models目录

使用:

  1. 如果只是简单地获取值,比如有一个src/models/user.ts,默认导出 user,则使用方式:<div>{useModel('user')}</div>
  2. 如果想使用暴露出的一部分方法去更改状态值,可以添加第二个参数,是一个函数

建立一个 counter.ts 文件

import { useState, useCallback } from 'react';

export default () => {
const [counter, setCounter] = useState(0);
const increment = useCallback(() => setCounter(c => c + 1), []);
const decrement = useCallback(() => setCounter(c => c - 1), []);
return { counter, increment, decrement };
};
import { useModel } from '@umijs/max';

export default const App = () => {
const { add, minus } = useModel('counter', (ret) => ({
add: ret.increment,
minus: ret.decrement
}));

return (
<div>
<button type="button" onClick={add}>
add by 1
</button>
<button type="button" onClick={minus}>
minus by 1
</button>
</div>
);
}
  1. 在 app.tsx 中,有一个 getInitialState 方法,可以初始化全局状态
import { useModel } from '@umijs/max';

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

return <h1>{initialState?.title}</h1>;

刷新 initialState

const { refresh } = useModel('@@initialState');

refresh();

国际化

https://umijs.org/docs/max/i18n

菜单

在 locales 的 zh-CN (中文)的 menu.ts,以及 en-US(英文)的 menu.ts 中增加上面新添加的 Home 页面国际化 key 与值

页面

使用 umi 自带的 FormattedMessage 组件,id 就是国际化的 key。还有一种方式是使用 useIntl 钩子函数,可以在方法中使用,更加灵活

import { FormattedMessage, useIntl } from '@umijs/max';

function Home() {
return (
<div className="home">
<FormattedMessage id="pages.home.title" />
</div>
);
}

export default Home;

const UpdateForm = props => {
const intl = useIntl();
return <h1>{intl.formatMessage({ id: 'pages.home.title' })}</h1>;
};

开发规范

  1. 所有路由组件(会配置在路由配置中的组件)以大驼峰命名打平到 pages 下面第一级。不建议在路由组件内部再嵌套路由组件

踩坑记录

安装 @ant-design/charts 报错

如果按照官方文档安装主包,会因为 antd 的版本问题报错,相关 Issue。安装对应的子包即可。常用子包如下:

  • 统计图表:@ant-design/plots
  • 地图:@ant-design/maps
  • 流程图:@ant-design/flowchart
  • 关系图:@ant-design/graphs
# 不推荐的安装方式
npm install @ant-design/charts --save

# 推荐的安装方式
npm install @ant-design/plots -S

报错 Module "xxx" does not exist in container

例如报错:Module "./@ant-design/plots" does not exist in container. while loading "./@ant-design/plots" from webpack/container/reference/mf

解决方案一:在 config/config.ts 里注释或者删除 mfsu: {}

解决方案二:删除src/.umi 这个文件夹,重启项目

权限方案

权限配置文件路径src/access.ts

页面内的权限控制

路由和菜单的权限控制:

先定义src/access.ts, src/app.ts,再在路由配置项上添加 access 属性即可完成路由和菜单的权限控制。

access 属性的值为 src/access.ts 中返回的对象的 key

如果鉴权函数在接收路由作为参数后返回值为 false,该条路由将会被禁用,并且从左侧 layout 菜单中移除,如果直接从 URL 访问对应路由,将看到一个 403 页面

菜单权限

按钮权限

Access 组件

import { Access, useAccess } from '@umijs/max';

const TableList = () => {
const access = useAccess();

return (
<Access accessible={access.hasPerms('system:user:add')} fallback={<div>Can not read</div>}>
<a
onClick={() => {
console.log(123);
}}
>
click
</a>
</Access>
);
};

export default TableList;

hidden 属性

<a
hidden={!access.hasPerms('system:user:edit')}
onClick={() => {
console.log(123);
}}
>
click
</a>

这种方式不推荐,虽然也能在无权限时起到隐藏按钮的作用,但是会在 DOM 中存在

核心实现

src/access.ts
/**
* @see https://umijs.org/zh-CN/plugins/plugin-access
* */

import { checkRole, matchPermission } from './utils/permission';

export default function access(initialState: { currentUser?: System.CurrentUser } | undefined) {
const { currentUser } = initialState ?? {};
const hasPerms = (perm: string) => {
return matchPermission(initialState?.currentUser?.permissions, perm);
};
const roleFiler = (route: { authority: string[] }) => {
return checkRole(initialState?.currentUser?.roles, route.authority);
};
return {
canAdmin: currentUser && currentUser.access === 'admin',
hasPerms,
roleFiler
};
}
src/utils/permission.ts
// /**
// * 字符权限校验
// * @param {Array} value 校验值
// * @returns {Boolean}
// */
export function matchPerms(permissions: string[], value: string[]) {
if (value && value instanceof Array && value.length > 0) {
const permissionDatas = value;
const all_permission = '*:*:*';
const hasPermission = permissions.some(permission => {
return all_permission === permission || permissionDatas.includes(permission);
});
if (!hasPermission) {
return false;
}
return true;
}
console.error(`need roles! Like checkPermi="['system:user:add','system:user:edit']"`);
return false;
}

export function matchPerm(permissions: string[], value: string) {
if (value && value.length > 0) {
const permissionDatas = value;
const all_permission = '*:*:*';
const hasPermission = permissions.some(permission => {
return all_permission === permission || permissionDatas === permission;
});
if (!hasPermission) {
return false;
}
return true;
}
console.error(`need roles! Like checkPermi="['system:user:add','system:user:edit']"`);
return false;
}

export function matchPermission(permissions: string[] | undefined, value: any): boolean {
if (permissions === undefined) return false;
const type = typeof value;
if (type === 'string') {
return matchPerm(permissions, value);
}
return matchPerms(permissions, value);
}

/**
* 角色权限校验
* @param {Array} value 校验值
* @returns {Boolean}
*/
interface roleProps {
roleCode: string;
roleId: number;
roleName: string;
permissions?: any;
}
export function checkRole(roles: roleProps[] | undefined, value: string[]) {
if (roles && value && value.length > 0) {
for (let i = 0; i < roles?.length; i++) {
for (let j = 0; j < value?.length; j++) {
if (value[j] === roles[i].roleCode) {
return true;
}
}
}
}
console.error(`need roles! Like checkRole="['admin','editor']"`);
return false;
}
config/defaultSettings.ts
import { ProLayoutProps } from '@ant-design/pro-components';

const Settings: ProLayoutProps & {
pwa?: boolean;
logo?: string;
} = {
navTheme: 'light',
colorPrimary: '#1890ff',
layout: 'mix',
contentWidth: 'Fluid',
fixedHeader: false,
fixSiderbar: true,
colorWeak: false,
title: '标题',
pwa: true,
// logo: 'https://gw.alipayobjects.com/zos/rmsportal/KDpgvguMpGfqaHPjicRK.svg',
iconfontUrl: '',
token: {}
};

export default Settings;

如果要使用在线 logo,直接在这里给 logo 赋值就行

如果要使用本地图片,需要将config/defaultSettings.ts的 logo 字段注释掉,并在src/app.tsx配置RunTimeLayoutConfig

export const layout: RunTimeLayoutConfig = ({ initialState, setInitialState }) => ({
logo: 'logo.png'
});

动态显示菜单图标

使用:

import Icon from './Icon';

<ProForm>
<Form.Item name="icon" label="图标" style={{ width: '50%' }}>
<Icon
value={form.getFieldValue('icon')}
onChange={val => {
form.setFieldValue('icon', val);
}}
/>
</Form.Item>
</ProForm>;
Icon.tsx
import { Input, Popover } from 'antd';
import React, { useEffect, useState } from 'react';
import * as allIcons from '@ant-design/icons';
import { useEmotionCss } from '@ant-design/use-emotion-css';

type IconProps = {
value: string;
onChange: (value: string) => void;
};

const Icon = (props: IconProps) => {
const [openPopover, setOpenPopover] = useState<boolean>(false);
const [iconValue, setIconValue] = useState<string>();

useEffect(() => {
setIconValue(props.value);
}, [props.value]);

const iconStyle = useEmotionCss(() => {
return {
padding: '6px',
fontSize: '20px',
cursor: 'pointer',
transition: 'color 0.3s',
'&:hover': {
backgroundColor: '#eee'
}
};
});

const iconSelect = () => {
const enums: { [key: string]: React.ReactNode } = {};
const showType = typeof allIcons['IdcardOutlined'];
const iconKeys = Object.keys(allIcons);
const pattern = /(Outlined)/; // /(Filled|TwoTone)/

for (let i = 0; i < iconKeys.length; i++) {
const key = iconKeys[i];
const item = allIcons[key];

if (item.hasOwnProperty('displayName') && typeof item === showType && item.displayName !== 'AntdIcon') {
if (item.render?.name && pattern.test(item.render.name)) {
const iconNode = React.createElement(item);
if (iconNode) {
enums[key] = iconNode;
}
}
}
}

return enums;
};

const iconSelected = (name: string) => {
props.onChange(name);
setIconValue(name);
setOpenPopover(false);
};

const onInputChange = (e: any) => {
props.onChange(e.target.value);
};

const content = () => {
const icon = iconSelect();

return (
<div
style={{
display: 'flex',
flexWrap: 'wrap',
width: '300px',
height: '400px',
overflow: 'auto'
}}
>
{Object.keys(icon).map((e, i) => {
return (
<div key={i} className={iconStyle} onClick={() => iconSelected(e)}>
{React.createElement(allIcons[e])}
</div>
);
})}
</div>
);
};

const handleOpenChange = (newOpen: boolean) => {
setOpenPopover(newOpen);
};

return (
<Popover
placement="bottom"
title="选择图标"
content={content}
trigger="click"
open={openPopover}
onOpenChange={handleOpenChange}
>
<Input value={iconValue} placeholder="点击选择图标" allowClear onChange={onInputChange} />
</Popover>
);
};

export default Icon;

修改 app.ts,让图标能够渲染出来

import * as allIcons from '@ant-design/icons';
import type { MenuDataItem } from '@ant-design/pro-components';

const loopMenuItem = (menus: any[]): MenuDataItem[] =>
menus.map(({ icon, routes, ...item }) => ({
...item,
icon: icon && React.createElement(allIcons[icon]),
children: routes && loopMenuItem(routes)
}));

export const layout: RunTimeLayoutConfig = ({ initialState, setInitialState }) => {
return {
menu: {
params: {
accountId: initialState?.currentUser?.accountId
},
request: async () => {
const menuData = await fetchMenuData();
return loopMenuItem(menuData);
}
},
menuHeaderRender: undefined,
childrenRender: children => {
return <>{children}</>;
},
...initialState?.settings
};
};