一、项目起步
1、下载pro-cli脚手架并且创建项目:
npm i @ant-design/pro-cli -g
pro create myapp
2、安装依赖
cd myapp && npm install
cd myapp && yarn
3、启动项目
npm run start
此时项目将会启动在 http://localhost:8000
二、项目的基本样式修改
在config/defaultSettings.ts中我们可以看到如下:
const settings: LayoutSettings & {
pwa?: boolean;
logo?: string;
} = {
// 修改左上角的 logo
logo: 'https://gw.alipayobjects.com/zos/rmsportal/KDpgvguMpGfqaHPjicRK.svg',
// 设置标题的 title
title: 'Ant Design Pro',
navTheme: 'light',
// 拂晓蓝
primaryColor: '#1890ff',
//导航布局方式
layout: 'mix',
//layout 的内容模式,Fluid:自适应,Fixed:定宽 1200px
contentWidth: 'Fluid',
fixSiderbar: true,
};
export default settings;
三、处理登录页
1、处理登录页基本样式及表单
在登录页E:\laixinyu\demo\mydemo\src\pages\User\Login\index.tsx中直接利用现成的表单进行修改:
- LoginForm - 表单组件
- ProFormText - 输入框组件
- ProFormText.Password - 密码输入组件
需要注意的是ProFormText或者ProFormText.Password中的name属性将作为后面接收表单提交的值的属性名
2、提交表单数据并登录
我们可以在LoginForm中绑定onFinish属性,他将会在我们提交表单数据时执行对应的函数:
const submitdata = (val)=>{
console.log(val)
/**
*这里的val是一个对象包含了我们在表单中输入的数据
*{ username:admin, password:123 }
*我们在这里接收到数据后 将会调用登录接口并且将数据传入
*/
}
<LoginForm
onFinish={submitdata}
>
<LoginForm/>
在登录之后,我们要通过useModel将token以及之后获取的用户信息存入在model中,并且需要将token做持久化处理
3、创建Model(简易数据流)
在src下新建一个文件夹model,新建文件user.ts,并且内容如下:
import { useState, useCallback } from 'react';
import { setLocalToken, clearLocalToken } from '@/utils';
// 用户信息
export default () => {
const [token, setToken] = useState('');
const [userInfo, setUserInfo] = useState({});
// 设置token
const addToken = useCallback((token) => {
setToken(token);
setLocalToken(token);
}, []);
// 设置userInfo
const addUserInfo = useCallback((userInfo) => setUserInfo(userInfo), []);
return { token, userInfo, addToken, addUserInfo };
};
4、使用Model--useModel
注意:useModel('模块名'),这里的模块名为文件名
如:user.ts => useModel('user)
import { useModel } from '@umijs/max';
import React from 'react'
export Login:React.FC = ()=>{
const user = useModel('user')
//调用user中的setToken方法设置token
user.setToken(token)
//同时,也可以获取数据
const token = user.token //这里就可以获取到token的值
}
5、处理currentUser-访问权限
通过在initialState中设置currentUser使其通过路由鉴权
const { initialState, setInitialState } = useModel('@@initialState');
//在登录并且获得用户数据userInfo后 将用户数据存入currentUser
setInitialState({
...initialState,
//这里的access属性主要是为了鉴权使用
currentUser:{...userInfo,access:userInfo.role[0]}
})
除此之外,我们还需要在app.tsx中处理currentUser,这种情况主要是当用户直接访问/admin页的时候,就会去做鉴权,如果当前存在token并且它还未失效,那么就会被允许进入到/admin页面
// 如果不是登录页面,执行
if (window.location.pathname !== loginPath) {
//当访问页面不是login页的时候 去鉴权
//拉取用户信息 存入到initialState的currentUser中
let currentUser = await fetchUserInfo();
if (currentUser) {
currentUser = { ...currentUser, access: currentUser.roles[0] };
}
const getRoutes = await getMenu();
const setRoutes = getRoutes.map((item) => {
return {
name: item.title,
icon: iconList[item.icon === 'sys-tools' ? 'sysTools' : item.icon],
path: `/${item.path}`,
routes: item.children && loopRoutes(item.children),
};
});
return {
fetchUserInfo,
currentUser,
routes: setRoutes,
settings: defaultSettings,
};
}
这样一来 我们就成功登录,并且进入到/admin的页面
四、处理Admin-Layout页面的样式
在ProLayout中进行配置
// ProLayout 支持的api https://procomponents.ant.design/components/layout
export const layout: RunTimeLayoutConfig = ({ initialState, setInitialState }) => {
return {
token: {
//顶部header样式
header: {
heightLayoutHeader: 60,
},
//侧边栏样式
sider: {
colorBgCollapsedButton: '#fff',
colorTextCollapsedButtonHover: 'rgba(0,0,0,0.65)',
colorTextCollapsedButton: 'rgba(0,0,0,0.45)',
colorMenuBackground: '#195ffe',
colorBgMenuItemCollapsedHover: 'rgba(0,0,0,0.06)',
colorBgMenuItemCollapsedSelected: 'rgba(0,0,0,0.15)',
colorMenuItemDivider: 'rgba(255,255,255,0.15)',
colorBgMenuItemHover: 'rgba(0,0,0,0.06)',
colorBgMenuItemSelected: 'rgba(0,0,0,0.15)',
colorTextMenuSelected: '#fff',
colorTextMenuItemHover: 'rgba(255,255,255,0.75)',
colorTextMenu: 'rgba(255,255,255,1)',
colorTextMenuSecondary: 'rgba(255,255,255,0.65)',
colorTextMenuTitle: 'rgba(255,255,255,0.95)',
colorTextMenuActive: 'rgba(255,255,255,0.95)',
colorTextSubMenuSelected: '#fff',
},
},
//侧边栏宽度
siderWidth: 200,
//右上侧内容 例如用户头像等
rightContentRender: () => <RightContent />,
waterMarkProps: {
content: initialState?.currentUser?.name,
},
footerRender: () => <Footer />,
onPageChange: () => {
const { location } = history;
// 如果没有登录,重定向到 login
if (!initialState?.currentUser && location.pathname !== loginPath) {
history.push(loginPath);
}
},
layoutBgImgList: [
{
src: 'https://mdn.alipayobjects.com/yuyan_qk0oxh/afts/img/D2LWSqNny4sAAAAAAAAAAAAAFl94AQBr',
left: 85,
bottom: 100,
height: '303px',
},
{
src: 'https://mdn.alipayobjects.com/yuyan_qk0oxh/afts/img/C2TWRpJpiC0AAAAAAAAAAAAAFl94AQBr',
bottom: -68,
right: -45,
height: '303px',
},
{
src: 'https://mdn.alipayobjects.com/yuyan_qk0oxh/afts/img/F6vSTbj8KpYAAAAAAAAAAAAAFl94AQBr',
bottom: 0,
left: 0,
width: '331px',
},
],
links: isDev ? [] : [],
menuHeaderRender: undefined,
// 自定义 403 页面
// unAccessible: <div>unAccessible</div>,
// 增加一个 loading 的状态
childrenRender: (children, props) => {
// if (initialState?.loading) return <PageLoading />;
return (
<>
{children}
{!props.location?.pathname?.includes('/login') && (
<SettingDrawer
disableUrlParams
enableDarkTheme
settings={initialState?.settings}
onSettingChange={(settings) => {
setInitialState((preInitialState) => ({
...preInitialState,
settings,
}));
}}
/>
)}
</>
);
},
...initialState?.settings,
};
};
五、动态控制菜单
在这里我们主要通过ProLayout中的postMenuData属性或者menu来动态设置菜单(从服务器拉取菜单内容)
eg:
postMenuData:initialState?.routes
menu: { //推荐
params: initialState,
request: async () => {
return initialState?.routes;
},
},
1、首先,我们在app.tsx中的getInitialState中定义初始化菜单数据routes
// 递归处理路由方法
const loopRoutes = (routes) => {
const newRes = routes.map((item) => {
return {
name: item.title,
icon: iconList[item.icon === 'sys-tools' ? 'sysTools' : item.icon],
path: `/${item.path}`,
routes: item.children && loopRoutes(item.children),
};
});
return newRes;
};
export async function getInitialState(): Promise<{
settings?: Partial<LayoutSettings>;
currentUser?: API.CurrentUser;
loading?: boolean;
//菜单数组-定义 +
routes?: MenuDataItem[]; // +
fetchUserInfo?: () => Promise<API.CurrentUser | undefined>;
}>{
...
...
//获取最新菜单
const getRoutes = await getroutes()
if(getRoutes){
//重组菜单数据
const setRoutes = getRoutes.map((item) => {
return {
name: item.title,
icon: iconList[item.icon === 'sys-tools' ? 'sysTools' : item.icon],
path: `/${item.path}`,
//因为有些存在子菜单,所以进行递归处理
routes: item.children && loopRoutes(item.children),
};
});
//然后将获取到的菜单数据加入到routes中
return {
routes:setRoutes
}
}
}
2、除此之外,我们还需要在登录成功后,获取路由。
在/Login/index.tsx中,在登陆成功后 调用获取菜单的API,然后通过setInitialState一并将用户数据和菜单数据加入其中:
// 登录
try {
const res = await userLogin(formData);
// 持久化token
token_model.addToken(res.token);
// 加载用户信息添加权限
const userInfo = await fetchUserInfo();
const getRoutes = await addRoutes();
message.success('登录成功');
// 加载菜单
setInitialState({
...initialState,
//菜单数据
routes: getRoutes,
//用户信息
currentUser: { ...userInfo, access: userInfo.roles[0] },
});
} catch (err) {
message.error(err.response.data);
}
//跳转路由
history.push('/admin');
六、退出登录
我们在src\components\RightContent\AvatarDropdown.tsx中,退出登录时需要清除本地token值以及initialState中的用户数据和菜单数据
目前遇到的问题
1、在局部less中书写或覆盖ant组件样式,不生效:
解决:
在局部less文件中使用global
:global(.ant-tag){
background-color:red;
}