= T | false;
35 |
36 | export interface RouterTypes extends Omit {
37 | computedMatch?: match;
38 | route?: Route;
39 | location: BasicRouteProps['location'] | { pathname?: string };
40 | }
41 |
42 | export interface MessageDescriptor {
43 | id: any;
44 | description?: string;
45 | defaultMessage?: string;
46 | }
47 |
--------------------------------------------------------------------------------
/example/tests/PuppeteerEnvironment.js:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line
2 | const NodeEnvironment = require('jest-environment-node');
3 | const getBrowser = require('./getBrowser');
4 |
5 | class PuppeteerEnvironment extends NodeEnvironment {
6 | // Jest is not available here, so we have to reverse engineer
7 | // the setTimeout function, see https://github.com/facebook/jest/blob/v23.1.0/packages/jest-runtime/src/index.js#L823
8 | setTimeout(timeout) {
9 | if (this.global.jasmine) {
10 | // eslint-disable-next-line no-underscore-dangle
11 | this.global.jasmine.DEFAULT_TIMEOUT_INTERVAL = timeout;
12 | } else {
13 | this.global[Symbol.for('TEST_TIMEOUT_SYMBOL')] = timeout;
14 | }
15 | }
16 |
17 | async setup() {
18 | const browser = await getBrowser();
19 | const page = await browser.newPage();
20 | this.global.browser = browser;
21 | this.global.page = page;
22 | }
23 |
24 | async teardown() {
25 | const { page, browser } = this.global;
26 |
27 | if (page) {
28 | await page.close();
29 | }
30 |
31 | if (browser) {
32 | await browser.disconnect();
33 | }
34 |
35 | if (browser) {
36 | await browser.close();
37 | }
38 | }
39 | }
40 |
41 | module.exports = PuppeteerEnvironment;
42 |
--------------------------------------------------------------------------------
/example/src/pages/user/login/components/Login/LoginTab.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react';
2 | import { TabPaneProps } from 'antd/es/tabs';
3 | import { Tabs } from 'antd';
4 | import LoginContext, { LoginContextProps } from './LoginContext';
5 |
6 | const { TabPane } = Tabs;
7 |
8 | const generateId = (() => {
9 | let i = 0;
10 | return (prefix = '') => {
11 | i += 1;
12 | return `${prefix}${i}`;
13 | };
14 | })();
15 |
16 | interface LoginTabProps extends TabPaneProps {
17 | tabUtil: LoginContextProps['tabUtil'];
18 | active?: boolean;
19 | }
20 |
21 | const LoginTab: React.FC = (props) => {
22 | useEffect(() => {
23 | const uniqueId = generateId('login-tab-');
24 | const { tabUtil } = props;
25 | if (tabUtil) {
26 | tabUtil.addTab(uniqueId);
27 | }
28 | }, []);
29 | const { children } = props;
30 | return {props.active && children};
31 | };
32 |
33 | const WrapContext: React.FC & {
34 | typeName: string;
35 | } = (props) => (
36 |
37 | {(value) => }
38 |
39 | );
40 |
41 | // 标志位 用来判断是不是自定义组件
42 | WrapContext.typeName = 'LoginTab';
43 |
44 | export default WrapContext;
45 |
--------------------------------------------------------------------------------
/tests/__tests__/defaultSettings.ts:
--------------------------------------------------------------------------------
1 | import { MenuProps } from 'antd/es/menu';
2 |
3 | export type ContentWidth = 'Fluid' | 'Fixed';
4 |
5 | export interface Settings {
6 | /**
7 | * theme for nav menu
8 | */
9 | navTheme: MenuProps['theme'] | undefined;
10 | /**
11 | * nav menu position: `sidemenu` or `topmenu`
12 | */
13 | layout: 'sidemenu' | 'topmenu';
14 | /**
15 | * layout of content: `Fluid` or `Fixed`, only works when layout is topmenu
16 | */
17 | contentWidth: ContentWidth;
18 | /**
19 | * sticky header
20 | */
21 | fixedHeader: boolean;
22 |
23 | /**
24 | * sticky siderbar
25 | */
26 | fixSiderbar: boolean;
27 | menu: { locale: boolean };
28 | title: string;
29 | // Your custom iconfont Symbol script Url
30 | // eg://at.alicdn.com/t/font_1039637_btcrd5co4w.js
31 | // 注意:如果需要图标多色,Iconfont 图标项目里要进行批量去色处理
32 | // Usage: https://github.com/ant-design/ant-design-pro/pull/3517
33 | iconfontUrl: string;
34 | }
35 |
36 | const defaultSettings: Settings = {
37 | navTheme: 'dark',
38 | layout: 'sidemenu',
39 | contentWidth: 'Fluid',
40 | fixedHeader: false,
41 | fixSiderbar: false,
42 | menu: {
43 | locale: true,
44 | },
45 | title: 'Ant Design Pro',
46 | iconfontUrl: '',
47 | };
48 | export default defaultSettings;
49 |
--------------------------------------------------------------------------------
/docs/example/IconFont.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | // eslint-disable-next-line import/no-unresolved
3 | import ProLayout, { PageContainer } from '@ant-design/pro-layout';
4 |
5 | export default () => (
6 |
12 |
44 |
45 | Hello World
46 |
47 |
48 |
49 | );
50 |
--------------------------------------------------------------------------------
/example/tests/getBrowser.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable global-require */
2 | /* eslint-disable import/no-extraneous-dependencies */
3 | const findChrome = require('carlo/lib/find_chrome');
4 |
5 | const getBrowser = async () => {
6 | try {
7 | // eslint-disable-next-line import/no-unresolved
8 | const puppeteer = require('puppeteer');
9 | const browser = await puppeteer.launch({
10 | args: [
11 | '--disable-gpu',
12 | '--disable-dev-shm-usage',
13 | '--no-first-run',
14 | '--no-zygote',
15 | '--no-sandbox',
16 | ],
17 | });
18 | return browser;
19 | } catch (error) {
20 | // console.log(error)
21 | }
22 |
23 | try {
24 | // eslint-disable-next-line import/no-unresolved
25 | const puppeteer = require('puppeteer-core');
26 | const findChromePath = await findChrome({});
27 | const { executablePath } = findChromePath;
28 | const browser = await puppeteer.launch({
29 | executablePath,
30 | args: [
31 | '--disable-gpu',
32 | '--disable-dev-shm-usage',
33 | '--no-first-run',
34 | '--no-zygote',
35 | '--no-sandbox',
36 | ],
37 | });
38 | return browser;
39 | } catch (error) {
40 | console.log('🧲 no find chrome');
41 | }
42 | throw new Error('no find puppeteer');
43 | };
44 |
45 | module.exports = getBrowser;
46 |
--------------------------------------------------------------------------------
/example/src/locales/zh-CN/settingDrawer.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | 'app.setting.pagestyle': '整体风格设置',
3 | 'app.setting.pagestyle.dark': '暗色菜单风格',
4 | 'app.setting.pagestyle.light': '亮色菜单风格',
5 | 'app.setting.content-width': '内容区域宽度',
6 | 'app.setting.content-width.fixed': '定宽',
7 | 'app.setting.content-width.fluid': '流式',
8 | 'app.setting.themecolor': '主题色',
9 | 'app.setting.themecolor.dust': '薄暮',
10 | 'app.setting.themecolor.volcano': '火山',
11 | 'app.setting.themecolor.sunset': '日暮',
12 | 'app.setting.themecolor.cyan': '明青',
13 | 'app.setting.themecolor.green': '极光绿',
14 | 'app.setting.themecolor.daybreak': '拂晓蓝(默认)',
15 | 'app.setting.themecolor.geekblue': '极客蓝',
16 | 'app.setting.themecolor.purple': '酱紫',
17 | 'app.setting.navigationmode': '导航模式',
18 | 'app.setting.sidemenu': '侧边菜单布局',
19 | 'app.setting.topmenu': '顶部菜单布局',
20 | 'app.setting.fixedheader': '固定 Header',
21 | 'app.setting.fixedsidebar': '固定侧边菜单',
22 | 'app.setting.fixedsidebar.hint': '侧边菜单布局时可配置',
23 | 'app.setting.hideheader': '下滑时隐藏 Header',
24 | 'app.setting.hideheader.hint': '固定 Header 时可配置',
25 | 'app.setting.othersettings': '其他设置',
26 | 'app.setting.weakmode': '色弱模式',
27 | 'app.setting.copy': '拷贝设置',
28 | 'app.setting.copyinfo': '拷贝成功,请到 src/defaultSettings.js 中替换默认配置',
29 | 'app.setting.production.hint':
30 | '配置栏只在开发环境用于预览,生产环境不会展现,请拷贝后手动修改配置文件',
31 | };
32 |
--------------------------------------------------------------------------------
/example/src/locales/zh-TW/settingDrawer.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | 'app.setting.pagestyle': '整體風格設置',
3 | 'app.setting.pagestyle.dark': '暗色菜單風格',
4 | 'app.setting.pagestyle.light': '亮色菜單風格',
5 | 'app.setting.content-width': '內容區域寬度',
6 | 'app.setting.content-width.fixed': '定寬',
7 | 'app.setting.content-width.fluid': '流式',
8 | 'app.setting.themecolor': '主題色',
9 | 'app.setting.themecolor.dust': '薄暮',
10 | 'app.setting.themecolor.volcano': '火山',
11 | 'app.setting.themecolor.sunset': '日暮',
12 | 'app.setting.themecolor.cyan': '明青',
13 | 'app.setting.themecolor.green': '極光綠',
14 | 'app.setting.themecolor.daybreak': '拂曉藍(默認)',
15 | 'app.setting.themecolor.geekblue': '極客藍',
16 | 'app.setting.themecolor.purple': '醬紫',
17 | 'app.setting.navigationmode': '導航模式',
18 | 'app.setting.sidemenu': '側邊菜單布局',
19 | 'app.setting.topmenu': '頂部菜單布局',
20 | 'app.setting.fixedheader': '固定 Header',
21 | 'app.setting.fixedsidebar': '固定側邊菜單',
22 | 'app.setting.fixedsidebar.hint': '側邊菜單布局時可配置',
23 | 'app.setting.hideheader': '下滑時隱藏 Header',
24 | 'app.setting.hideheader.hint': '固定 Header 時可配置',
25 | 'app.setting.othersettings': '其他設置',
26 | 'app.setting.weakmode': '色弱模式',
27 | 'app.setting.copy': '拷貝設置',
28 | 'app.setting.copyinfo': '拷貝成功,請到 src/defaultSettings.js 中替換默認配置',
29 | 'app.setting.production.hint':
30 | '配置欄只在開發環境用於預覽,生產環境不會展現,請拷貝後手動修改配置文件',
31 | };
32 |
--------------------------------------------------------------------------------
/src/utils/getMenuData.ts:
--------------------------------------------------------------------------------
1 | import { transformRoute } from '@umijs/route-utils';
2 |
3 | import { MenuDataItem, Route, MessageDescriptor } from '../typings';
4 |
5 | function fromEntries(iterable: any) {
6 | return [...iterable].reduce(
7 | (
8 | obj: {
9 | [key: string]: MenuDataItem;
10 | },
11 | [key, val],
12 | ) => {
13 | // eslint-disable-next-line no-param-reassign
14 | obj[key] = val;
15 | return obj;
16 | },
17 | {},
18 | );
19 | }
20 |
21 | export default (
22 | routes: Route[],
23 | menu?: { locale?: boolean },
24 | formatMessage?: (message: MessageDescriptor) => string,
25 | menuDataRender?: (menuData: MenuDataItem[]) => MenuDataItem[],
26 | ) => {
27 | const { menuData, breadcrumb } = transformRoute(
28 | routes,
29 | menu?.locale || false,
30 | formatMessage,
31 | true,
32 | );
33 | if (!menuDataRender) {
34 | return {
35 | breadcrumb: fromEntries(breadcrumb),
36 | breadcrumbMap: breadcrumb,
37 | menuData,
38 | };
39 | }
40 | const renderData = transformRoute(
41 | menuDataRender(menuData),
42 | menu?.locale || false,
43 | formatMessage,
44 | true,
45 | );
46 | return {
47 | breadcrumb: fromEntries(renderData.breadcrumb),
48 | breadcrumbMap: renderData.breadcrumb,
49 | menuData: renderData.menuData,
50 | };
51 | };
52 |
--------------------------------------------------------------------------------
/example/tests/beforeTest.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable global-require */
2 | /* eslint-disable import/no-extraneous-dependencies */
3 | const { execSync } = require('child_process');
4 | const { join } = require('path');
5 | const findChrome = require('carlo/lib/find_chrome');
6 | const detectInstaller = require('detect-installer');
7 |
8 | const installPuppeteer = () => {
9 | // find can use package manger
10 | const packages = detectInstaller(join(__dirname, '../../'));
11 | // get installed package manger
12 | const packageName = packages.find(detectInstaller.hasPackageCommand) || 'npm';
13 | console.log(`🤖 will use ${packageName} install puppeteer`);
14 | const command = `${packageName} ${packageName.includes('yarn') ? 'add' : 'i'} puppeteer`;
15 | execSync(command, {
16 | stdio: 'inherit',
17 | });
18 | };
19 |
20 | const initPuppeteer = async () => {
21 | try {
22 | // eslint-disable-next-line import/no-unresolved
23 | const findChromePath = await findChrome({});
24 | const { executablePath } = findChromePath;
25 | console.log(`🧲 find you browser in ${executablePath}`);
26 | return;
27 | } catch (error) {
28 | console.log('🧲 no find chrome');
29 | }
30 |
31 | try {
32 | require.resolve('puppeteer');
33 | } catch (error) {
34 | // need install puppeteer
35 | await installPuppeteer();
36 | }
37 | };
38 |
39 | initPuppeteer();
40 |
--------------------------------------------------------------------------------
/src/GridContent/index.tsx:
--------------------------------------------------------------------------------
1 | import './GridContent.less';
2 |
3 | import React, { useContext, CSSProperties } from 'react';
4 | import classNames from 'classnames';
5 |
6 | import RouteContext from '../RouteContext';
7 | import { PureSettings } from '../defaultSettings';
8 |
9 | interface GridContentProps {
10 | contentWidth?: PureSettings['contentWidth'];
11 | children: React.ReactNode;
12 | className?: string;
13 | style?: CSSProperties;
14 | prefixCls?: string;
15 | }
16 |
17 | /**
18 | * This component can support contentWidth so you don't need to calculate the width
19 | * contentWidth=Fixed, width will is 1200
20 | * @param props
21 | */
22 | const GridContent: React.SFC = (props) => {
23 | const value = useContext(RouteContext);
24 | const {
25 | children,
26 | contentWidth: propsContentWidth,
27 | className: propsClassName,
28 | style,
29 | prefixCls = 'ant-pro',
30 | } = props;
31 | const contentWidth = propsContentWidth || value.contentWidth;
32 | let className = `${prefixCls}-grid-content`;
33 | if (contentWidth === 'Fixed') {
34 | className = `${prefixCls}-grid-content wide`;
35 | }
36 | return (
37 |
40 | );
41 | };
42 |
43 | export default GridContent;
44 |
--------------------------------------------------------------------------------
/docs/example/BreadcrumbsRepeat.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | // eslint-disable-next-line import/no-unresolved
3 | import ProLayout, { PageContainer } from '@ant-design/pro-layout';
4 |
5 | export default () => (
6 |
12 |
[
17 | {
18 | path: '/welcome',
19 | name: '欢迎',
20 | },
21 | {
22 | path: '/admin',
23 | name: '管理',
24 | children: [
25 | {
26 | name: '申请单列表',
27 | path: '/admin/process',
28 | },
29 | {
30 | name: '申请单详情',
31 | path: '/admin/process/detail/:id',
32 | hideInMenu: true,
33 | },
34 | {
35 | name: '编辑申请单',
36 | path: '/admin/process/edit/:id',
37 | hideInMenu: true,
38 | },
39 | {
40 | name: '添加申请单',
41 | path: '/admin/process/add',
42 | hideInMenu: true,
43 | },
44 | ],
45 | },
46 | ]}
47 | >
48 |
49 | Hello World
50 |
51 |
52 |
53 | );
54 |
--------------------------------------------------------------------------------
/src/SettingDrawer/RegionalChange.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Switch, List } from 'antd';
3 | import { ProSettings } from '../defaultSettings';
4 | import { getFormatMessage } from './index';
5 | import { renderLayoutSettingItem } from './LayoutChange';
6 |
7 | const RegionalSetting: React.FC<{
8 | settings: Partial;
9 | changeSetting: (key: string, value: any, hideLoading?: boolean) => void;
10 | }> = ({ settings = {}, changeSetting }) => {
11 | const formatMessage = getFormatMessage();
12 | const regionalSetting = ['header', 'footer', 'menu', 'menuHeader'];
13 | return (
14 | {
18 | return {
19 | title: formatMessage({ id: `app.setting.regionalsettings.${key}` }),
20 | action: (
21 |
28 | changeSetting(
29 | `${key}Render`,
30 | checked === true ? undefined : false,
31 | )
32 | }
33 | />
34 | ),
35 | };
36 | })}
37 | />
38 | );
39 | };
40 |
41 | export default RegionalSetting;
42 |
--------------------------------------------------------------------------------
/docs/demo/materialMenu.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | // eslint-disable-next-line import/no-unresolved
3 | import ProLayout, { PageContainer } from '@ant-design/pro-layout';
4 | import List from '@material-ui/core/List';
5 | import ListItemText from '@material-ui/core/ListItemText';
6 | import ListItem from '@material-ui/core/ListItem';
7 |
8 | export default () => (
9 | (
17 |
24 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 | )}
44 | >
45 | Hello World
46 |
47 | );
48 |
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | import BasicLayout, { BasicLayoutProps } from './BasicLayout';
2 | import DefaultHeader, { HeaderViewProps as HeaderProps } from './Header';
3 | import TopNavHeader, { TopNavHeaderProps } from './TopNavHeader';
4 | import SettingDrawer, {
5 | SettingDrawerProps,
6 | SettingDrawerState,
7 | } from './SettingDrawer';
8 |
9 | import DefaultFooter, { FooterProps } from './Footer';
10 | import GridContent from './GridContent';
11 | import PageContainer from './PageContainer';
12 | import RouteContext, { RouteContextType } from './RouteContext';
13 | import getMenuData from './utils/getMenuData';
14 | import getPageTitle from './getPageTitle';
15 | import PageLoading from './PageLoading';
16 | import FooterToolbar from './FooterToolbar';
17 |
18 | export type { ProSettings as Settings, ProSettings } from './defaultSettings';
19 |
20 | export type { MenuDataItem } from './typings';
21 |
22 | const PageHeaderWrapper = PageContainer;
23 |
24 | export {
25 | BasicLayout,
26 | RouteContext,
27 | PageLoading,
28 | GridContent,
29 | DefaultHeader,
30 | TopNavHeader,
31 | DefaultFooter,
32 | SettingDrawer,
33 | getPageTitle,
34 | PageHeaderWrapper,
35 | getMenuData,
36 | PageContainer,
37 | FooterToolbar,
38 | };
39 |
40 | export type {
41 | FooterProps,
42 | TopNavHeaderProps,
43 | BasicLayoutProps,
44 | RouteContextType,
45 | HeaderProps,
46 | SettingDrawerProps,
47 | SettingDrawerState,
48 | };
49 |
50 | export default BasicLayout;
51 |
--------------------------------------------------------------------------------
/src/locales/zh-TW/settingDrawer.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | 'app.setting.pagestyle': '整體風格設置',
3 | 'app.setting.pagestyle.dark': '暗色菜單風格',
4 | 'app.setting.pagestyle.light': '亮色菜單風格',
5 | 'app.setting.content-width': '內容區域寬度',
6 | 'app.setting.content-width.fixed': '定寬',
7 | 'app.setting.content-width.fluid': '流式',
8 | 'app.setting.themecolor': '主題色',
9 | 'app.setting.themecolor.dust': '薄暮',
10 | 'app.setting.themecolor.volcano': '火山',
11 | 'app.setting.themecolor.sunset': '日暮',
12 | 'app.setting.themecolor.cyan': '明青',
13 | 'app.setting.themecolor.green': '極光綠',
14 | 'app.setting.themecolor.daybreak': '拂曉藍(默認)',
15 | 'app.setting.themecolor.geekblue': '極客藍',
16 | 'app.setting.themecolor.purple': '醬紫',
17 | 'app.setting.navigationmode': '導航模式',
18 | 'app.setting.sidemenu': '側邊菜單布局',
19 | 'app.setting.topmenu': '頂部菜單布局',
20 | 'app.setting.mixmenu': '混合菜單布局',
21 | 'app.setting.splitMenus': '自动分割菜单',
22 | 'app.setting.fixedheader': '固定 Header',
23 | 'app.setting.fixedsidebar': '固定側邊菜單',
24 | 'app.setting.fixedsidebar.hint': '側邊菜單布局時可配置',
25 | 'app.setting.hideheader': '下滑時隱藏 Header',
26 | 'app.setting.hideheader.hint': '固定 Header 時可配置',
27 | 'app.setting.othersettings': '其他設置',
28 | 'app.setting.weakmode': '色弱模式',
29 | 'app.setting.copy': '拷貝設置',
30 | 'app.setting.loading': '正在加載主題',
31 | 'app.setting.copyinfo':
32 | '拷貝成功,請到 src/defaultSettings.js 中替換默認配置',
33 | 'app.setting.production.hint':
34 | '配置欄只在開發環境用於預覽,生產環境不會展現,請拷貝後手動修改配置文件',
35 | };
36 |
--------------------------------------------------------------------------------
/docs/demo/customizeMenu.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | // eslint-disable-next-line import/no-unresolved
3 | import ProLayout, { PageContainer } from '@ant-design/pro-layout';
4 | import { Button } from 'antd';
5 | import defaultProps from './defaultProps';
6 |
7 | export default () => {
8 | const [index, setIndex] = useState(0);
9 | return (
10 | <>
11 |
19 | (
21 |
22 | {index} {dom}
23 |
24 | )}
25 | subMenuItemRender={(_, dom) => (
26 |
27 | {index} {dom}
28 |
29 | )}
30 | title="Remax"
31 | logo="https://gw.alipayobjects.com/mdn/rms_b5fcc5/afts/img/A*1NHAQYduQiQAAAAAAAAAAABkARQnAQ"
32 | menuHeaderRender={(logo, title) => (
33 |
42 | )}
43 | {...defaultProps}
44 | location={{
45 | pathname: '/welcome',
46 | }}
47 | >
48 | Hello World
49 |
50 | >
51 | );
52 | };
53 |
--------------------------------------------------------------------------------
/src/SettingDrawer/BlockCheckbox.tsx:
--------------------------------------------------------------------------------
1 | import { CheckOutlined } from '@ant-design/icons';
2 | import { Tooltip } from 'antd';
3 |
4 | import React, { useState, useEffect } from 'react';
5 |
6 | export interface BlockCheckboxProps {
7 | value: string;
8 | onChange: (key: string) => void;
9 | list?: {
10 | title: string;
11 | key: string;
12 | url: string;
13 | }[];
14 | prefixCls: string;
15 | }
16 |
17 | const BlockCheckbox: React.FC = ({
18 | value,
19 | onChange,
20 | list,
21 | prefixCls,
22 | }) => {
23 | const baseClassName = `${prefixCls}-drawer-block-checkbox`;
24 | const [dom, setDom] = useState([]);
25 | useEffect(() => {
26 | const domList = (list || []).map((item) => (
27 | onChange(item.key)}
31 | >
32 |
33 |
34 |
35 |
41 |
42 |
43 |
44 | ));
45 | setDom(domList);
46 | }, [value, list?.length]);
47 | return (
48 |
54 | {dom}
55 |
56 | );
57 | };
58 |
59 | export default BlockCheckbox;
60 |
--------------------------------------------------------------------------------
/src/GlobalFooter/index.tsx:
--------------------------------------------------------------------------------
1 | import './index.less';
2 |
3 | import React from 'react';
4 | import classNames from 'classnames';
5 | import { WithFalse } from '../typings';
6 |
7 | export interface GlobalFooterProps {
8 | links?: WithFalse<
9 | {
10 | key?: string;
11 | title: React.ReactNode;
12 | href: string;
13 | blankTarget?: boolean;
14 | }[]
15 | >;
16 | copyright?: React.ReactNode;
17 | style?: React.CSSProperties;
18 | prefixCls?: string;
19 | className?: string;
20 | }
21 |
22 | export default ({
23 | className,
24 | prefixCls = 'ant-pro',
25 | links,
26 | copyright,
27 | style,
28 | }: GlobalFooterProps) => {
29 | if (
30 | (links == null ||
31 | links === false ||
32 | (Array.isArray(links) && links.length === 0)) &&
33 | (copyright == null || copyright === false)
34 | ) {
35 | return null;
36 | }
37 | const baseClassName = `${prefixCls}-global-footer`;
38 | const clsString = classNames(baseClassName, className);
39 | return (
40 |
59 | );
60 | };
61 |
--------------------------------------------------------------------------------
/docs/demo/dynamicMenu.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect } from 'react';
2 | import ProLayout, {
3 | PageContainer,
4 | MenuDataItem,
5 | // eslint-disable-next-line import/no-unresolved
6 | } from '@ant-design/pro-layout';
7 | import { Button, Spin } from 'antd';
8 | import customMenuDate from './customMenu';
9 |
10 | export default () => {
11 | const [menuData, setMenuData] = useState([]);
12 | const [loading, setLoading] = useState(true);
13 | const [index, setIndex] = useState(0);
14 | useEffect(() => {
15 | setMenuData([]);
16 | setLoading(true);
17 | setTimeout(() => {
18 | setMenuData(customMenuDate);
19 | setLoading(false);
20 | }, 2000);
21 | }, [index]);
22 | return (
23 | <>
24 |
32 |
38 | loading ? (
39 |
44 | {dom}
45 |
46 | ) : (
47 | dom
48 | )
49 | }
50 | location={{
51 | pathname: '/welcome',
52 | }}
53 | menuDataRender={() => menuData}
54 | >
55 | Hello World
56 |
57 | >
58 | );
59 | };
60 |
--------------------------------------------------------------------------------
/example/src/locales/en-US/settingDrawer.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | 'app.setting.pagestyle': 'Page style setting',
3 | 'app.setting.pagestyle.dark': 'Dark style',
4 | 'app.setting.pagestyle.light': 'Light style',
5 | 'app.setting.content-width': 'Content Width',
6 | 'app.setting.content-width.fixed': 'Fixed',
7 | 'app.setting.content-width.fluid': 'Fluid',
8 | 'app.setting.themecolor': 'Theme Color',
9 | 'app.setting.themecolor.dust': 'Dust Red',
10 | 'app.setting.themecolor.volcano': 'Volcano',
11 | 'app.setting.themecolor.sunset': 'Sunset Orange',
12 | 'app.setting.themecolor.cyan': 'Cyan',
13 | 'app.setting.themecolor.green': 'Polar Green',
14 | 'app.setting.themecolor.daybreak': 'Daybreak Blue (default)',
15 | 'app.setting.themecolor.geekblue': 'Geek Blue',
16 | 'app.setting.themecolor.purple': 'Golden Purple',
17 | 'app.setting.navigationmode': 'Navigation Mode',
18 | 'app.setting.sidemenu': 'Side Menu Layout',
19 | 'app.setting.topmenu': 'Top Menu Layout',
20 | 'app.setting.fixedheader': 'Fixed Header',
21 | 'app.setting.fixedsidebar': 'Fixed Sidebar',
22 | 'app.setting.fixedsidebar.hint': 'Works on Side Menu Layout',
23 | 'app.setting.hideheader': 'Hidden Header when scrolling',
24 | 'app.setting.hideheader.hint': 'Works when Hidden Header is enabled',
25 | 'app.setting.othersettings': 'Other Settings',
26 | 'app.setting.weakmode': 'Weak Mode',
27 | 'app.setting.copy': 'Copy Setting',
28 | 'app.setting.copyinfo': 'copy success,please replace defaultSettings in src/models/setting.js',
29 | 'app.setting.production.hint':
30 | 'Setting panel shows in development environment only, please manually modify',
31 | };
32 |
--------------------------------------------------------------------------------
/example/src/components/RightContent/index.less:
--------------------------------------------------------------------------------
1 | @import '~antd/es/style/themes/default.less';
2 |
3 | @pro-header-hover-bg: rgba(0, 0, 0, 0.025);
4 |
5 | .menu {
6 | :global(.anticon) {
7 | margin-right: 8px;
8 | }
9 | :global(.ant-dropdown-menu-item) {
10 | min-width: 160px;
11 | }
12 | }
13 |
14 | .right {
15 | display: flex;
16 | float: right;
17 | margin-left: auto;
18 | overflow: hidden;
19 | .action {
20 | display: flex;
21 | align-items: center;
22 | padding: 0 12px;
23 | cursor: pointer;
24 | transition: all 0.3s;
25 |
26 | &:hover {
27 | background: @pro-header-hover-bg;
28 | }
29 | &:global(.opened) {
30 | background: @pro-header-hover-bg;
31 | }
32 | }
33 | .search {
34 | padding: 0 12px;
35 | &:hover {
36 | background: transparent;
37 | }
38 | }
39 | .account {
40 | .avatar {
41 | margin-right: 8px;
42 | color: @primary-color;
43 | vertical-align: top;
44 | background: rgba(255, 255, 255, 0.85);
45 | }
46 | }
47 | }
48 |
49 | .dark {
50 | .action {
51 | &:hover {
52 | background: #252a3d;
53 | }
54 | &:global(.opened) {
55 | background: #252a3d;
56 | }
57 | }
58 | }
59 |
60 | @media only screen and (max-width: @screen-md) {
61 | :global(.ant-divider-vertical) {
62 | vertical-align: unset;
63 | }
64 | .name {
65 | display: none;
66 | }
67 | .right {
68 | position: absolute;
69 | top: 0;
70 | right: 12px;
71 | .account {
72 | .avatar {
73 | margin-right: 0;
74 | }
75 | }
76 | .search {
77 | display: none;
78 | }
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/docs/example/searchMenu.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 |
3 | import ProLayout, {
4 | PageContainer,
5 | MenuDataItem,
6 | // eslint-disable-next-line import/no-unresolved
7 | } from '@ant-design/pro-layout';
8 | import { Input } from 'antd';
9 | import complexMenu from './complexMenu';
10 |
11 | const filterByMenuDate = (
12 | data: MenuDataItem[],
13 | keyWord: string,
14 | ): MenuDataItem[] =>
15 | data
16 | .map((item) => {
17 | if (
18 | (item.name && item.name.includes(keyWord)) ||
19 | filterByMenuDate(item.children || [], keyWord).length > 0
20 | ) {
21 | return {
22 | ...item,
23 | children: filterByMenuDate(item.children || [], keyWord),
24 | };
25 | }
26 |
27 | return undefined;
28 | })
29 | .filter((item) => item) as MenuDataItem[];
30 |
31 | export default () => {
32 | const [keyWord, setKeyWord] = useState('');
33 | return (
34 |
40 |
45 | !collapsed && (
46 | {
48 | setKeyWord(e);
49 | }}
50 | />
51 | )
52 | }
53 | menuDataRender={() => complexMenu}
54 | postMenuData={(menus) => filterByMenuDate(menus || [], keyWord)}
55 | >
56 |
57 | Hello World
58 |
59 |
60 |
61 | );
62 | };
63 |
--------------------------------------------------------------------------------
/docs/demo/antd@3MenuIconFormServe.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | // eslint-disable-next-line import/no-unresolved
3 | import ProLayout, { PageContainer, MenuDataItem } from '@ant-design/pro-layout';
4 | import { Icon } from '@ant-design/compatible';
5 |
6 | const defaultMenus = [
7 | {
8 | path: '/',
9 | name: 'welcome',
10 | icon: 'smile',
11 | children: [
12 | {
13 | path: '/welcome',
14 | name: 'one',
15 | icon: 'smile',
16 | children: [
17 | {
18 | path: '/welcome/welcome',
19 | name: 'two',
20 | icon: 'smile',
21 | exact: true,
22 | },
23 | ],
24 | },
25 | ],
26 | },
27 | {
28 | path: '/demo',
29 | name: 'demo',
30 | icon: 'heart',
31 | },
32 | ];
33 |
34 | const loopMenuItem = (menus: MenuDataItem[]): MenuDataItem[] =>
35 | menus.map(({ icon, children, ...item }) => ({
36 | ...item,
37 | icon: icon && ,
38 | children: children && loopMenuItem(children),
39 | }));
40 |
41 | export default () => (
42 |
49 |
loopMenuItem(defaultMenus)}
58 | >
59 |
60 |
65 | Hello World
66 |
67 |
68 |
69 |
70 | );
71 |
--------------------------------------------------------------------------------
/example/tests/run-tests.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable eslint-comments/disable-enable-pair */
2 | /* eslint-disable @typescript-eslint/no-var-requires */
3 | /* eslint-disable eslint-comments/no-unlimited-disable */
4 | const { spawn } = require('child_process');
5 | // eslint-disable-next-line import/no-extraneous-dependencies
6 | const { kill } = require('cross-port-killer');
7 |
8 | const env = Object.create(process.env);
9 | env.BROWSER = 'none';
10 | env.TEST = true;
11 | env.UMI_UI = 'none';
12 | env.PROGRESS = 'none';
13 | // flag to prevent multiple test
14 | let once = false;
15 |
16 | const startServer = spawn(/^win/.test(process.platform) ? 'npm.cmd' : 'npm', ['start'], {
17 | env,
18 | });
19 |
20 | startServer.stderr.on('data', (data) => {
21 | // eslint-disable-next-line
22 | console.log(data.toString());
23 | });
24 |
25 | startServer.on('exit', () => {
26 | kill(process.env.PORT || 8000);
27 | });
28 |
29 | console.log('Starting development server for e2e tests...');
30 | startServer.stdout.on('data', (data) => {
31 | console.log(data.toString());
32 | // hack code , wait umi
33 | if (
34 | (!once && data.toString().indexOf('Compiled successfully') >= 0) ||
35 | data.toString().indexOf('Theme generated successfully') >= 0
36 | ) {
37 | // eslint-disable-next-line
38 | once = true;
39 | console.log('Development server is started, ready to run tests.');
40 | const testCmd = spawn(
41 | /^win/.test(process.platform) ? 'npm.cmd' : 'npm',
42 | ['test', '--', '--maxWorkers=1', '--runInBand'],
43 | {
44 | stdio: 'inherit',
45 | },
46 | );
47 | testCmd.on('exit', (code) => {
48 | startServer.kill();
49 | process.exit(code);
50 | });
51 | }
52 | });
53 |
--------------------------------------------------------------------------------
/src/locales/zh-CN/settingDrawer.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | 'app.setting.pagestyle': '整体风格设置',
3 | 'app.setting.pagestyle.dark': '暗色菜单风格',
4 | 'app.setting.pagestyle.light': '亮色菜单风格',
5 | 'app.setting.content-width': '内容区域宽度',
6 | 'app.setting.content-width.fixed': '定宽',
7 | 'app.setting.content-width.fluid': '流式',
8 | 'app.setting.themecolor': '主题色',
9 | 'app.setting.themecolor.dust': '薄暮',
10 | 'app.setting.themecolor.volcano': '火山',
11 | 'app.setting.themecolor.sunset': '日暮',
12 | 'app.setting.themecolor.cyan': '明青',
13 | 'app.setting.themecolor.green': '极光绿',
14 | 'app.setting.themecolor.daybreak': '拂晓蓝(默认)',
15 | 'app.setting.themecolor.geekblue': '极客蓝',
16 | 'app.setting.themecolor.purple': '酱紫',
17 | 'app.setting.navigationmode': '导航模式',
18 | 'app.setting.regionalsettings': '内容区域',
19 | 'app.setting.regionalsettings.header': '顶栏',
20 | 'app.setting.regionalsettings.menu': '菜单',
21 | 'app.setting.regionalsettings.footer': '页脚',
22 | 'app.setting.regionalsettings.menuHeader': '菜单头',
23 | 'app.setting.sidemenu': '侧边菜单布局',
24 | 'app.setting.topmenu': '顶部菜单布局',
25 | 'app.setting.mixmenu': '混合菜单布局',
26 | 'app.setting.splitMenus': '自动分割菜单',
27 | 'app.setting.fixedheader': '固定 Header',
28 | 'app.setting.fixedsidebar': '固定侧边菜单',
29 | 'app.setting.fixedsidebar.hint': '侧边菜单布局时可配置',
30 | 'app.setting.hideheader': '下滑时隐藏 Header',
31 | 'app.setting.hideheader.hint': '固定 Header 时可配置',
32 | 'app.setting.othersettings': '其他设置',
33 | 'app.setting.weakmode': '色弱模式',
34 | 'app.setting.copy': '拷贝设置',
35 | 'app.setting.loading': '正在加载主题',
36 | 'app.setting.copyinfo':
37 | '拷贝成功,请到 src/defaultSettings.js 中替换默认配置',
38 | 'app.setting.production.hint':
39 | '配置栏只在开发环境用于预览,生产环境不会展现,请拷贝后手动修改配置文件',
40 | };
41 |
--------------------------------------------------------------------------------
/example/src/components/SelectLang/index.tsx:
--------------------------------------------------------------------------------
1 | import { GlobalOutlined } from '@ant-design/icons';
2 | import { Menu } from 'antd';
3 | import { getLocale, setLocale } from 'umi';
4 | import { ClickParam } from 'antd/es/menu';
5 | import React from 'react';
6 | import classNames from 'classnames';
7 | import HeaderDropdown from '../HeaderDropdown';
8 | import styles from './index.less';
9 |
10 | interface SelectLangProps {
11 | className?: string;
12 | }
13 |
14 | const SelectLang: React.FC = (props) => {
15 | const { className } = props;
16 | const selectedLang = getLocale();
17 |
18 | const changeLang = ({ key }: ClickParam): void => setLocale(key, false);
19 |
20 | const locales = ['zh-CN', 'zh-TW', 'en-US', 'pt-BR'];
21 | const languageLabels = {
22 | 'zh-CN': '简体中文',
23 | 'zh-TW': '繁体中文',
24 | 'en-US': 'English',
25 | 'pt-BR': 'Português',
26 | };
27 | const languageIcons = {
28 | 'zh-CN': '🇨🇳',
29 | 'zh-TW': '🇭🇰',
30 | 'en-US': '🇺🇸',
31 | 'pt-BR': '🇧🇷',
32 | };
33 | const langMenu = (
34 |
44 | );
45 | return (
46 |
47 |
48 |
49 |
50 |
51 | );
52 | };
53 |
54 | export default SelectLang;
55 |
--------------------------------------------------------------------------------
/src/defaultSettings.ts:
--------------------------------------------------------------------------------
1 | import { MenuTheme } from 'antd/es/menu/MenuContext';
2 |
3 | export type ContentWidth = 'Fluid' | 'Fixed';
4 |
5 | export interface RenderSetting {
6 | headerRender?: false;
7 | footerRender?: false;
8 | menuRender?: false;
9 | menuHeaderRender?: false;
10 | }
11 | export interface PureSettings {
12 | /**
13 | * theme for nav menu
14 | */
15 | navTheme: MenuTheme | 'realDark' | undefined;
16 | /**
17 | * nav menu position: `side` or `top`
18 | */
19 | headerHeight?: number;
20 | /**
21 | * customize header height
22 | */
23 | layout: 'side' | 'top' | 'mix';
24 | /**
25 | * layout of content: `Fluid` or `Fixed`, only works when layout is top
26 | */
27 | contentWidth: ContentWidth;
28 | /**
29 | * sticky header
30 | */
31 | fixedHeader: boolean;
32 | /**
33 | * sticky siderbar
34 | */
35 | fixSiderbar: boolean;
36 | menu: { locale?: boolean; defaultOpenAll?: boolean };
37 | title: string;
38 | // Your custom iconfont Symbol script Url
39 | // eg://at.alicdn.com/t/font_1039637_btcrd5co4w.js
40 | // 注意:如果需要图标多色,Iconfont 图标项目里要进行批量去色处理
41 | // Usage: https://github.com/ant-design/ant-design-pro/pull/3517
42 | iconfontUrl: string;
43 | primaryColor: string;
44 | colorWeak?: boolean;
45 | splitMenus?: boolean;
46 | }
47 |
48 | export type ProSettings = PureSettings & RenderSetting;
49 |
50 | const defaultSettings: ProSettings = {
51 | navTheme: 'dark',
52 | layout: 'side',
53 | contentWidth: 'Fluid',
54 | fixedHeader: false,
55 | fixSiderbar: false,
56 | menu: {
57 | locale: true,
58 | },
59 | headerHeight: 48,
60 | title: 'Ant Design Pro',
61 | iconfontUrl: '',
62 | primaryColor: '#1890ff',
63 | };
64 | export default defaultSettings;
65 |
--------------------------------------------------------------------------------
/src/Footer.tsx:
--------------------------------------------------------------------------------
1 | import { CopyrightOutlined, GithubOutlined } from '@ant-design/icons';
2 | import { Layout } from 'antd';
3 | import React, { Fragment, CSSProperties } from 'react';
4 |
5 | import GlobalFooter from './GlobalFooter';
6 | import { WithFalse } from './typings';
7 |
8 | const { Footer } = Layout;
9 |
10 | const defaultLinks = [
11 | {
12 | key: 'Ant Design Pro',
13 | title: 'Ant Design Pro',
14 | href: 'https://pro.ant.design',
15 | blankTarget: true,
16 | },
17 | {
18 | key: 'github',
19 | title: ,
20 | href: 'https://github.com/ant-design/ant-design-pro',
21 | blankTarget: true,
22 | },
23 | {
24 | key: 'Ant Design',
25 | title: 'Ant Design',
26 | href: 'https://ant.design',
27 | blankTarget: true,
28 | },
29 | ];
30 |
31 | const defaultCopyright = '2019 蚂蚁金服体验技术部出品';
32 |
33 | export interface FooterProps {
34 | links?: WithFalse<
35 | {
36 | key?: string;
37 | title: React.ReactNode;
38 | href: string;
39 | blankTarget?: boolean;
40 | }[]
41 | >;
42 | copyright?: WithFalse;
43 | style?: CSSProperties;
44 | className?: string;
45 | }
46 |
47 | const FooterView: React.FC = ({
48 | links,
49 | copyright,
50 | style,
51 | className,
52 | }: FooterProps) => (
53 |
65 | );
66 |
67 | export default FooterView;
68 |
--------------------------------------------------------------------------------
/example/src/locales/pt-BR/settingDrawer.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | 'app.setting.pagestyle': 'Configuração de estilo da página',
3 | 'app.setting.pagestyle.dark': 'Dark style',
4 | 'app.setting.pagestyle.light': 'Light style',
5 | 'app.setting.content-width': 'Largura do conteúdo',
6 | 'app.setting.content-width.fixed': 'Fixo',
7 | 'app.setting.content-width.fluid': 'Fluido',
8 | 'app.setting.themecolor': 'Cor do Tema',
9 | 'app.setting.themecolor.dust': 'Poeira Vermelha',
10 | 'app.setting.themecolor.volcano': 'Vulcão',
11 | 'app.setting.themecolor.sunset': 'Sunset Orange',
12 | 'app.setting.themecolor.cyan': 'Ciano',
13 | 'app.setting.themecolor.green': 'Verde Polar',
14 | 'app.setting.themecolor.daybreak': 'Alvorada Azul (default)',
15 | 'app.setting.themecolor.geekblue': 'Geek Azul',
16 | 'app.setting.themecolor.purple': 'Roxo Dourado',
17 | 'app.setting.navigationmode': 'Modo de Navegação',
18 | 'app.setting.sidemenu': 'Layout do Menu Lateral',
19 | 'app.setting.topmenu': 'Layout do Menu Superior',
20 | 'app.setting.fixedheader': 'Cabeçalho fixo',
21 | 'app.setting.fixedsidebar': 'Barra lateral fixa',
22 | 'app.setting.fixedsidebar.hint': 'Funciona no layout do menu lateral',
23 | 'app.setting.hideheader': 'Esconder o cabeçalho quando rolar',
24 | 'app.setting.hideheader.hint': 'Funciona quando o esconder cabeçalho está abilitado',
25 | 'app.setting.othersettings': 'Outras configurações',
26 | 'app.setting.weakmode': 'Weak Mode',
27 | 'app.setting.copy': 'Copiar Configuração',
28 | 'app.setting.copyinfo':
29 | 'copiado com sucesso,por favor trocar o defaultSettings em src/models/setting.js',
30 | 'app.setting.production.hint':
31 | 'O painel de configuração apenas é exibido no ambiente de desenvolvimento, por favor modifique manualmente o',
32 | };
33 |
--------------------------------------------------------------------------------
/src/BasicLayout.less:
--------------------------------------------------------------------------------
1 | @import '~antd/es/style/themes/default.less';
2 |
3 | @basicLayout-prefix-cls: ~'@{ant-prefix}-pro-basicLayout';
4 | @pro-layout-header-height: 48px;
5 |
6 | .@{basicLayout-prefix-cls} {
7 | // BFC
8 | display: flex;
9 | flex-direction: column;
10 | width: 100%;
11 | min-height: 100%;
12 |
13 | .@{ant-prefix}-layout-header {
14 | &.@{ant-prefix}-pro-fixed-header {
15 | position: fixed;
16 | top: 0;
17 | }
18 | }
19 |
20 | &-content {
21 | position: relative;
22 | margin: 24px;
23 |
24 | .@{ant-prefix}-pro-page-container {
25 | margin: -24px -24px 0;
26 | }
27 |
28 | &-disable-margin {
29 | margin: 0;
30 |
31 | .@{ant-prefix}-pro-page-container {
32 | margin: 0;
33 | }
34 | }
35 | > .@{ant-prefix}-layout {
36 | max-height: 100%;
37 | }
38 | }
39 |
40 | // children should support fixed
41 | .@{basicLayout-prefix-cls}-is-children.@{basicLayout-prefix-cls}-fix-siderbar {
42 | height: 100vh;
43 | overflow: hidden;
44 | transform: rotate(0);
45 | }
46 |
47 | .@{basicLayout-prefix-cls}-has-header {
48 | // tech-page-container
49 | .tech-page-container {
50 | height: calc(100vh - @pro-layout-header-height);
51 | }
52 | .@{basicLayout-prefix-cls}-is-children.@{basicLayout-prefix-cls}-has-header {
53 | .tech-page-container {
54 | height: calc(
55 | 100vh - @pro-layout-header-height - @pro-layout-header-height;
56 | );
57 | }
58 | .@{basicLayout-prefix-cls}-is-children {
59 | min-height: calc(100vh - @pro-layout-header-height);
60 | &.@{basicLayout-prefix-cls}-fix-siderbar {
61 | height: calc(100vh - @pro-layout-header-height);
62 | }
63 | }
64 | }
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/example/src/pages/user/login/components/Login/map.tsx:
--------------------------------------------------------------------------------
1 | import { LockTwoTone, MailTwoTone, MobileTwoTone, UserOutlined } from '@ant-design/icons';
2 | import React from 'react';
3 | import styles from './index.less';
4 |
5 | export default {
6 | UserName: {
7 | props: {
8 | size: 'large',
9 | id: 'userName',
10 | prefix: (
11 |
17 | ),
18 | placeholder: 'admin',
19 | },
20 | rules: [
21 | {
22 | required: true,
23 | message: 'Please enter username!',
24 | },
25 | ],
26 | },
27 | Password: {
28 | props: {
29 | size: 'large',
30 | prefix: ,
31 | type: 'password',
32 | id: 'password',
33 | placeholder: '888888',
34 | },
35 | rules: [
36 | {
37 | required: true,
38 | message: 'Please enter password!',
39 | },
40 | ],
41 | },
42 | Mobile: {
43 | props: {
44 | size: 'large',
45 | prefix: ,
46 | placeholder: 'mobile number',
47 | },
48 | rules: [
49 | {
50 | required: true,
51 | message: 'Please enter mobile number!',
52 | },
53 | {
54 | pattern: /^1\d{10}$/,
55 | message: 'Wrong mobile number format!',
56 | },
57 | ],
58 | },
59 | Captcha: {
60 | props: {
61 | size: 'large',
62 | prefix: ,
63 | placeholder: 'captcha',
64 | },
65 | rules: [
66 | {
67 | required: true,
68 | message: 'Please enter Captcha!',
69 | },
70 | ],
71 | },
72 | };
73 |
--------------------------------------------------------------------------------
/docs/demo/antd@4MenuIconFormServe.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | // eslint-disable-next-line import/no-unresolved
3 | import ProLayout, { PageContainer, MenuDataItem } from '@ant-design/pro-layout';
4 | import { SmileOutlined, HeartOutlined } from '@ant-design/icons';
5 |
6 | const IconMap = {
7 | smile: ,
8 | heart: ,
9 | };
10 |
11 | const defaultMenus = [
12 | {
13 | path: '/',
14 | name: 'welcome',
15 | icon: 'smile',
16 | children: [
17 | {
18 | path: '/welcome',
19 | name: 'one',
20 | icon: 'smile',
21 | children: [
22 | {
23 | path: '/welcome/welcome',
24 | name: 'two',
25 | icon: 'smile',
26 | exact: true,
27 | },
28 | ],
29 | },
30 | ],
31 | },
32 | {
33 | path: '/demo',
34 | name: 'demo',
35 | icon: 'heart',
36 | },
37 | ];
38 |
39 | const loopMenuItem = (menus: MenuDataItem[]): MenuDataItem[] =>
40 | menus.map(({ icon, children, ...item }) => ({
41 | ...item,
42 | icon: icon && IconMap[icon as string],
43 | children: children && loopMenuItem(children),
44 | }));
45 |
46 | export default () => (
47 |
54 |
loopMenuItem(defaultMenus)}
63 | >
64 |
65 |
70 | Hello World
71 |
72 |
73 |
74 |
75 | );
76 |
--------------------------------------------------------------------------------
/example/src/locales/zh-TW/menu.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | 'menu.welcome': '歡迎',
3 | 'menu.more-blocks': '更多區塊',
4 | 'menu.home': '首頁',
5 | 'menu.login': '登錄',
6 | 'menu.admin': '权限',
7 | 'menu.admin.sub-page': '二级管理页',
8 | 'menu.exception.403': '403',
9 | 'menu.exception.404': '404',
10 | 'menu.exception.500': '500',
11 | 'menu.register': '註冊',
12 | 'menu.register.result': '註冊結果',
13 | 'menu.dashboard': 'Dashboard',
14 | 'menu.dashboard.analysis': '分析頁',
15 | 'menu.dashboard.monitor': '監控頁',
16 | 'menu.dashboard.workplace': '工作臺',
17 | 'menu.form': '表單頁',
18 | 'menu.form.basic-form': '基礎表單',
19 | 'menu.form.step-form': '分步表單',
20 | 'menu.form.step-form.info': '分步表單(填寫轉賬信息)',
21 | 'menu.form.step-form.confirm': '分步表單(確認轉賬信息)',
22 | 'menu.form.step-form.result': '分步表單(完成)',
23 | 'menu.form.advanced-form': '高級表單',
24 | 'menu.list': '列表頁',
25 | 'menu.list.table-list': '查詢表格',
26 | 'menu.list.basic-list': '標淮列表',
27 | 'menu.list.card-list': '卡片列表',
28 | 'menu.list.search-list': '搜索列表',
29 | 'menu.list.search-list.articles': '搜索列表(文章)',
30 | 'menu.list.search-list.projects': '搜索列表(項目)',
31 | 'menu.list.search-list.applications': '搜索列表(應用)',
32 | 'menu.profile': '詳情頁',
33 | 'menu.profile.basic': '基礎詳情頁',
34 | 'menu.profile.advanced': '高級詳情頁',
35 | 'menu.result': '結果頁',
36 | 'menu.result.success': '成功頁',
37 | 'menu.result.fail': '失敗頁',
38 | 'menu.account': '個人頁',
39 | 'menu.account.center': '個人中心',
40 | 'menu.account.settings': '個人設置',
41 | 'menu.account.trigger': '觸發報錯',
42 | 'menu.account.logout': '退出登錄',
43 | 'menu.exception': '异常页',
44 | 'menu.exception.not-permission': '403',
45 | 'menu.exception.not-find': '404',
46 | 'menu.exception.server-error': '500',
47 | 'menu.exception.trigger': '触发错误',
48 | 'menu.editor': '圖形編輯器',
49 | 'menu.editor.flow': '流程編輯器',
50 | 'menu.editor.mind': '腦圖編輯器',
51 | 'menu.editor.koni': '拓撲編輯器',
52 | };
53 |
--------------------------------------------------------------------------------
/src/TopNavHeader/index.less:
--------------------------------------------------------------------------------
1 | @import '~antd/es/style/themes/default.less';
2 | @import '../BasicLayout.less';
3 |
4 | @top-nav-header-prefix-cls: ~'@{ant-prefix}-pro-top-nav-header';
5 |
6 | .@{top-nav-header-prefix-cls} {
7 | position: relative;
8 | width: 100%;
9 | height: 100%;
10 | box-shadow: 0 1px 4px 0 rgba(0, 21, 41, 0.12);
11 | transition: background 0.3s, width 0.2s;
12 |
13 | :global {
14 | .ant-menu.ant-menu-dark .ant-menu-item-selected,
15 | .ant-menu-submenu-popup.ant-menu-dark .ant-menu-item-selected {
16 | background: #252a3d;
17 | }
18 | }
19 |
20 | .@{ant-prefix}-menu-submenu.@{ant-prefix}-menu-submenu-horizontal {
21 | height: 100%;
22 | .@{ant-prefix}-menu-submenu-title {
23 | height: 100%;
24 | }
25 | }
26 |
27 | &.light {
28 | background-color: @component-background;
29 | .@{top-nav-header-prefix-cls}-logo {
30 | h1 {
31 | color: @primary-color;
32 | }
33 | }
34 | .anticon {
35 | color: inherit;
36 | }
37 | }
38 |
39 | &-main {
40 | display: flex;
41 | height: 100%;
42 | padding-left: 16px;
43 | &-left {
44 | display: flex;
45 | min-width: 192px;
46 | }
47 | }
48 |
49 | .anticon {
50 | color: @btn-primary-color;
51 | }
52 |
53 | &-logo {
54 | position: relative;
55 | min-width: 165px;
56 | height: 100%;
57 | overflow: hidden;
58 | transition: all 0.3s;
59 | img {
60 | display: inline-block;
61 | height: 32px;
62 | vertical-align: middle;
63 | }
64 | h1 {
65 | display: inline-block;
66 | margin: 0 0 0 12px;
67 | color: @btn-primary-color;
68 | font-weight: 400;
69 | font-size: 16px;
70 | vertical-align: top;
71 | }
72 | }
73 | &-menu {
74 | min-width: 0;
75 | .@{ant-prefix}-menu.@{ant-prefix}-menu-horizontal {
76 | height: 100%;
77 | border: none;
78 | }
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/src/locales/it-IT/settingDrawer.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | 'app.setting.pagestyle': 'Impostazioni di stile',
3 | 'app.setting.pagestyle.dark': 'Tema scuro',
4 | 'app.setting.pagestyle.light': 'Tema chiaro',
5 | 'app.setting.content-width': 'Largezza contenuto',
6 | 'app.setting.content-width.fixed': 'Fissa',
7 | 'app.setting.content-width.fluid': 'Fluida',
8 | 'app.setting.themecolor': 'Colore del tema',
9 | 'app.setting.themecolor.dust': 'Rosso polvere',
10 | 'app.setting.themecolor.volcano': 'Vulcano',
11 | 'app.setting.themecolor.sunset': 'Arancione tramonto',
12 | 'app.setting.themecolor.cyan': 'Ciano',
13 | 'app.setting.themecolor.green': 'Verde polare',
14 | 'app.setting.themecolor.daybreak': 'Blu cielo mattutino (default)',
15 | 'app.setting.themecolor.geekblue': 'Blu geek',
16 | 'app.setting.themecolor.purple': 'Viola dorato',
17 | 'app.setting.navigationmode': 'Modalità di navigazione',
18 | 'app.setting.sidemenu': 'Menu laterale',
19 | 'app.setting.topmenu': 'Menu in testata',
20 | 'app.setting.mixmenu': 'Menu misto',
21 | 'app.setting.splitMenus': 'Menu divisi',
22 | 'app.setting.fixedheader': 'Testata fissa',
23 | 'app.setting.fixedsidebar': 'Menu laterale fisso',
24 | 'app.setting.fixedsidebar.hint': 'Solo se selezionato Menu laterale',
25 | 'app.setting.hideheader': 'Nascondi testata durante lo scorrimento',
26 | 'app.setting.hideheader.hint':
27 | 'Solo se abilitato Nascondi testata durante lo scorrimento',
28 | 'app.setting.othersettings': 'Altre impostazioni',
29 | 'app.setting.weakmode': 'Inverti colori',
30 | 'app.setting.copy': 'Copia impostazioni',
31 | 'app.setting.loading': 'Carico tema...',
32 | 'app.setting.copyinfo':
33 | 'Impostazioni copiate con successo! Incolla il contenuto in config/defaultSettings.js',
34 | 'app.setting.production.hint':
35 | 'Questo pannello è visibile solo durante lo sviluppo. Le impostazioni devono poi essere modificate manulamente',
36 | };
37 |
--------------------------------------------------------------------------------
/src/SettingDrawer/index.less:
--------------------------------------------------------------------------------
1 | @import '~antd/es/style/themes/default.less';
2 |
3 | @ant-pro-setting-drawer: ~'@{ant-prefix}-pro-setting-drawer';
4 |
5 | .@{ant-pro-setting-drawer} {
6 | &-content {
7 | position: relative;
8 | min-height: 100%;
9 | .@{ant-prefix}-list-item {
10 | span {
11 | flex: 1;
12 | }
13 | }
14 | }
15 |
16 | &-block-checkbox {
17 | display: flex;
18 | &-item {
19 | position: relative;
20 | margin-right: 16px;
21 | // box-shadow: 0 1px 1px 0 rgba(0, 0, 0, 0.1);
22 | border-radius: @border-radius-base;
23 | cursor: pointer;
24 | img {
25 | width: 48px;
26 | }
27 | }
28 | &-selectIcon {
29 | position: absolute;
30 | top: 0;
31 | right: 0;
32 | width: 100%;
33 | height: 100%;
34 | padding-top: 15px;
35 | padding-left: 24px;
36 | color: @primary-color;
37 | font-weight: bold;
38 | font-size: 14px;
39 | .action {
40 | color: @primary-color;
41 | }
42 | }
43 | }
44 |
45 | &-color_block {
46 | display: inline-block;
47 | width: 38px;
48 | height: 22px;
49 | margin: 4px;
50 | margin-right: 12px;
51 | vertical-align: middle;
52 | border-radius: 4px;
53 | cursor: pointer;
54 | }
55 |
56 | &-title {
57 | margin-bottom: 12px;
58 | color: @heading-color;
59 | font-size: 14px;
60 | line-height: 22px;
61 | }
62 |
63 | &-handle {
64 | position: absolute;
65 | top: 240px;
66 | right: 300px;
67 | z-index: 0;
68 | display: flex;
69 | align-items: center;
70 | justify-content: center;
71 | width: 48px;
72 | height: 48px;
73 | font-size: 16px;
74 | text-align: center;
75 | background: @primary-color;
76 | border-radius: 4px 0 0 4px;
77 | cursor: pointer;
78 | pointer-events: auto;
79 | }
80 |
81 | &-production-hint {
82 | margin-top: 16px;
83 | font-size: 12px;
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/docs/demo/defaultProps.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import {
3 | SmileOutlined,
4 | CrownOutlined,
5 | TabletOutlined,
6 | } from '@ant-design/icons';
7 |
8 | export default {
9 | route: {
10 | path: '/',
11 |
12 | routes: [
13 | {
14 | path: '/welcome',
15 | name: '欢迎',
16 | icon: ,
17 | component: './Welcome',
18 | },
19 | {
20 | path: '/admin',
21 | name: '管理页',
22 | icon: ,
23 | access: 'canAdmin',
24 | component: './Admin',
25 | routes: [
26 | {
27 | path: '/admin/sub-page',
28 | name: '一级页面',
29 | icon: ,
30 | component: './Welcome',
31 | },
32 | {
33 | path: '/admin/sub-page2',
34 | name: '二级页面',
35 | icon: ,
36 | component: './Welcome',
37 | },
38 | {
39 | path: '/admin/sub-page3',
40 | name: '三级页面',
41 | icon: ,
42 | component: './Welcome',
43 | },
44 | ],
45 | },
46 | {
47 | name: '列表页',
48 | icon: ,
49 | path: '/list',
50 | component: './ListTableList',
51 | routes: [
52 | {
53 | path: '/list/sub-page',
54 | name: '一级列表页面',
55 | icon: ,
56 | component: './Welcome',
57 | },
58 | {
59 | path: '/list/sub-page2',
60 | name: '二级列表页面',
61 | icon: ,
62 | component: './Welcome',
63 | },
64 | {
65 | path: '/list/sub-page3',
66 | name: '三级列表页面',
67 | icon: ,
68 | component: './Welcome',
69 | },
70 | ],
71 | },
72 | ],
73 | },
74 | location: {
75 | pathname: '/',
76 | },
77 | };
78 |
--------------------------------------------------------------------------------
/example/src/pages/Welcome.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { Card, Typography, Alert } from 'antd';
3 | import styles from './Welcome.less';
4 | import { PageContainer } from '../../../src/';
5 | import { useIntl } from 'umi';
6 |
7 | const CodePreview: React.FC<{}> = ({ children }) => (
8 |
9 |
10 | {children}
11 |
12 |
13 | );
14 |
15 | export default (): React.ReactNode => (
16 |
17 |
18 |
28 |
29 |
30 | {useIntl().formatMessage({ id: 'app.welcome.link.block-list' })}
31 |
32 |
33 | npm run ui
34 |
40 |
45 | {useIntl().formatMessage({ id: 'app.welcome.link.fetch-blocks' })}
46 |
47 |
48 | npm run fetch:blocks
49 |
50 |
56 | Want to add more pages? Please refer to{' '}
57 |
58 | use block
59 |
60 | 。
61 |
62 |
63 | );
64 |
--------------------------------------------------------------------------------
/src/locales/en-US/settingDrawer.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | 'app.setting.pagestyle': 'Page style setting',
3 | 'app.setting.pagestyle.dark': 'Dark style',
4 | 'app.setting.pagestyle.light': 'Light style',
5 | 'app.setting.content-width': 'Content Width',
6 | 'app.setting.content-width.fixed': 'Fixed',
7 | 'app.setting.content-width.fluid': 'Fluid',
8 | 'app.setting.themecolor': 'Theme Color',
9 | 'app.setting.themecolor.dust': 'Dust Red',
10 | 'app.setting.themecolor.volcano': 'Volcano',
11 | 'app.setting.themecolor.sunset': 'Sunset Orange',
12 | 'app.setting.themecolor.cyan': 'Cyan',
13 | 'app.setting.themecolor.green': 'Polar Green',
14 | 'app.setting.themecolor.daybreak': 'Daybreak Blue (default)',
15 | 'app.setting.themecolor.geekblue': 'Geek Blue',
16 | 'app.setting.themecolor.purple': 'Golden Purple',
17 | 'app.setting.navigationmode': 'Navigation Mode',
18 | 'app.setting.regionalsettings': 'Regional Settings',
19 | 'app.setting.regionalsettings.header': 'Header',
20 | 'app.setting.regionalsettings.menu': 'Menu',
21 | 'app.setting.regionalsettings.footer': 'Footer',
22 | 'app.setting.regionalsettings.menuHeader': 'Menu Header',
23 | 'app.setting.sidemenu': 'Side Menu Layout',
24 | 'app.setting.topmenu': 'Top Menu Layout',
25 | 'app.setting.mixmenu': 'Mix Menu Layout',
26 | 'app.setting.splitMenus': 'Split Menus',
27 | 'app.setting.fixedheader': 'Fixed Header',
28 | 'app.setting.fixedsidebar': 'Fixed Sidebar',
29 | 'app.setting.fixedsidebar.hint': 'Works on Side Menu Layout',
30 | 'app.setting.hideheader': 'Hidden Header when scrolling',
31 | 'app.setting.hideheader.hint': 'Works when Hidden Header is enabled',
32 | 'app.setting.othersettings': 'Other Settings',
33 | 'app.setting.weakmode': 'Weak Mode',
34 | 'app.setting.copy': 'Copy Setting',
35 | 'app.setting.loading': 'Loading theme',
36 | 'app.setting.copyinfo':
37 | 'copy success,please replace defaultSettings in src/models/setting.js',
38 | 'app.setting.production.hint':
39 | 'Setting panel shows in development environment only, please manually modify',
40 | };
41 |
--------------------------------------------------------------------------------
/tests/__tests__/__snapshots__/footer.test.tsx.snap:
--------------------------------------------------------------------------------
1 | // Jest Snapshot v1, https://goo.gl/fbAQLP
2 |
3 | exports[`DefaultFooter test copyright support false 1`] = `
4 |
57 | `;
58 |
--------------------------------------------------------------------------------
/example/src/locales/zh-CN/menu.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | 'menu.welcome': '欢迎',
3 | 'menu.more-blocks': '更多区块',
4 | 'menu.home': '首页',
5 | 'menu.admin': '管理页',
6 | 'menu.admin.sub-page': '二级管理页',
7 | 'menu.admin.sub-page2': '二级管理页',
8 | 'menu.admin.sub-page3': '三级管理页',
9 | 'menu.list.table-list.sub-page': '二级列表页',
10 | 'menu.list.table-list.sub-page2': '二级列表页',
11 | 'menu.list.table-list.sub-page3': '三级列表页',
12 | 'menu.login': '登录',
13 | 'menu.register': '注册',
14 | 'menu.register.result': '注册结果',
15 | 'menu.dashboard': 'Dashboard',
16 | 'menu.dashboard.analysis': '分析页',
17 | 'menu.dashboard.monitor': '监控页',
18 | 'menu.dashboard.workplace': '工作台',
19 | 'menu.exception.403': '403',
20 | 'menu.exception.404': '404',
21 | 'menu.exception.500': '500',
22 | 'menu.form': '表单页',
23 | 'menu.form.basic-form': '基础表单',
24 | 'menu.form.step-form': '分步表单',
25 | 'menu.form.step-form.info': '分步表单(填写转账信息)',
26 | 'menu.form.step-form.confirm': '分步表单(确认转账信息)',
27 | 'menu.form.step-form.result': '分步表单(完成)',
28 | 'menu.form.advanced-form': '高级表单',
29 | 'menu.list': '列表页',
30 | 'menu.list.table-list': '查询表格',
31 | 'menu.list.basic-list': '标准列表',
32 | 'menu.list.card-list': '卡片列表',
33 | 'menu.list.search-list': '搜索列表',
34 | 'menu.list.search-list.articles': '搜索列表(文章)',
35 | 'menu.list.search-list.projects': '搜索列表(项目)',
36 | 'menu.list.search-list.applications': '搜索列表(应用)',
37 | 'menu.profile': '详情页',
38 | 'menu.profile.basic': '基础详情页',
39 | 'menu.profile.advanced': '高级详情页',
40 | 'menu.result': '结果页',
41 | 'menu.result.success': '成功页',
42 | 'menu.result.fail': '失败页',
43 | 'menu.exception': '异常页',
44 | 'menu.exception.not-permission': '403',
45 | 'menu.exception.not-find': '404',
46 | 'menu.exception.server-error': '500',
47 | 'menu.exception.trigger': '触发错误',
48 | 'menu.account': '个人页',
49 | 'menu.account.center': '个人中心',
50 | 'menu.account.settings': '个人设置',
51 | 'menu.account.trigger': '触发报错',
52 | 'menu.account.logout': '退出登录',
53 | 'menu.editor': '图形编辑器',
54 | 'menu.editor.flow': '流程编辑器',
55 | 'menu.editor.mind': '脑图编辑器',
56 | 'menu.editor.koni': '拓扑编辑器',
57 | };
58 |
--------------------------------------------------------------------------------
/src/SettingDrawer/ThemeColor.tsx:
--------------------------------------------------------------------------------
1 | import './ThemeColor.less';
2 |
3 | import { CheckOutlined } from '@ant-design/icons';
4 |
5 | import { Tooltip } from 'antd';
6 |
7 | import React from 'react';
8 | import { genThemeToString } from '../utils/utils';
9 |
10 | export interface TagProps {
11 | color: string;
12 | check: boolean;
13 | className?: string;
14 | onClick?: () => void;
15 | }
16 |
17 | const Tag: React.FC = React.forwardRef(
18 | ({ color, check, ...rest }, ref) => (
19 |
20 | {check ? : ''}
21 |
22 | ),
23 | );
24 |
25 | export interface ThemeColorProps {
26 | colors?: {
27 | key: string;
28 | color: string;
29 | }[];
30 | value: string;
31 | onChange: (color: string) => void;
32 | formatMessage: (data: { id: any; defaultMessage?: string }) => string;
33 | }
34 |
35 | const ThemeColor: React.ForwardRefRenderFunction<
36 | HTMLDivElement,
37 | ThemeColorProps
38 | > = ({ colors, value, onChange, formatMessage }, ref) => {
39 | const colorList = colors || [];
40 | if (colorList.length < 1) {
41 | return null;
42 | }
43 | return (
44 |
45 |
46 | {colorList.map(({ key, color }) => {
47 | const themeKey = genThemeToString(key);
48 | return (
49 |
59 | onChange && onChange(key)}
64 | />
65 |
66 | );
67 | })}
68 |
69 |
70 | );
71 | };
72 |
73 | export default React.forwardRef(ThemeColor);
74 |
--------------------------------------------------------------------------------
/src/GlobalHeader/index.less:
--------------------------------------------------------------------------------
1 | @import '~antd/es/style/themes/default.less';
2 | @import '../BasicLayout.less';
3 |
4 | @pro-layout-global-header-prefix-cls: ~'@{ant-prefix}-pro-global-header';
5 |
6 | @pro-layout-header-bg: @component-background;
7 | @pro-layout-header-hover-bg: @component-background;
8 | @pro-layout-header-box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
9 |
10 | .@{pro-layout-global-header-prefix-cls} {
11 | position: relative;
12 | display: flex;
13 | align-items: center;
14 | height: 100%;
15 | padding: 0 16px;
16 | background: @pro-layout-header-bg;
17 | box-shadow: @pro-layout-header-box-shadow;
18 | > * {
19 | height: 100%;
20 | }
21 |
22 | &-collapsed-button {
23 | display: flex;
24 | align-items: center;
25 | margin-left: 16px;
26 | font-size: 20px;
27 | }
28 |
29 | &-layout {
30 | &-mix {
31 | background-color: @layout-sider-background;
32 | .@{pro-layout-global-header-prefix-cls}-logo {
33 | h1 {
34 | color: @btn-primary-color;
35 | }
36 | }
37 | .anticon {
38 | color: @btn-primary-color;
39 | }
40 | }
41 | }
42 |
43 | &-logo {
44 | position: relative;
45 | overflow: hidden;
46 | a {
47 | display: flex;
48 | align-items: center;
49 | height: 100%;
50 | img {
51 | height: 28px;
52 | }
53 | h1 {
54 | height: 32px;
55 | margin: 0 0 0 8px;
56 | margin: 0 0 0 12px;
57 | color: @primary-color;
58 | font-weight: 600;
59 | font-size: 18px;
60 | line-height: 32px;
61 | }
62 | }
63 | }
64 |
65 | &-menu {
66 | .anticon {
67 | margin-right: 8px;
68 | }
69 | .@{ant-prefix}-dropdown-menu-item {
70 | min-width: 160px;
71 | }
72 | }
73 |
74 | .dark {
75 | height: @pro-layout-header-height;
76 | .action {
77 | color: rgba(255, 255, 255, 0.85);
78 | > i {
79 | color: rgba(255, 255, 255, 0.85);
80 | }
81 | &:hover,
82 | &.opened {
83 | background: @primary-color;
84 | }
85 | .@{ant-prefix}-badge {
86 | color: rgba(255, 255, 255, 0.85);
87 | }
88 | }
89 | }
90 | }
91 |
--------------------------------------------------------------------------------
/tests/__tests__/pageHeaderWarp.test.tsx:
--------------------------------------------------------------------------------
1 | import { render, mount } from 'enzyme';
2 | import React from 'react';
3 | import ProLayout, { PageContainer } from '../../src';
4 | import defaultProps from './defaultProps';
5 | import { waitForComponentToPaint } from './util';
6 |
7 | describe('BasicLayout', () => {
8 | beforeAll(() => {
9 | Object.defineProperty(window, 'matchMedia', {
10 | value: jest.fn(() => ({
11 | matches: false,
12 | addListener() {},
13 | removeListener() {},
14 | })),
15 | });
16 | Object.defineProperty(window, 'localStorage', {
17 | value: {
18 | getItem: jest.fn(() => 'zh-CN'),
19 | },
20 | });
21 | });
22 |
23 | it('base use', () => {
24 | const html = render(
25 |
26 |
27 | ,
28 | );
29 | expect(html).toMatchSnapshot();
30 | });
31 |
32 | it('content is text', () => {
33 | const html = render(
34 |
35 |
36 | ,
37 | );
38 | expect(html).toMatchSnapshot();
39 | });
40 |
41 | it('title=false, don not render title view', async () => {
42 | const wrapper = mount(
43 |
44 |
45 | ,
46 | );
47 | await waitForComponentToPaint(wrapper);
48 | expect(wrapper.find('.ant-page-header-heading-title')).toHaveLength(0);
49 | });
50 |
51 | it('have default title', async () => {
52 | const wrapper = mount(
53 |
54 |
55 | ,
56 | );
57 | await waitForComponentToPaint(wrapper);
58 | const titleDom = wrapper.find('.ant-page-header-heading-title');
59 | expect(titleDom.text()).toEqual('welcome');
60 | });
61 |
62 | it('title overrides the default title', async () => {
63 | const wrapper = mount(
64 |
65 |
66 | ,
67 | );
68 | await waitForComponentToPaint(wrapper);
69 | const titleDom = wrapper.find('.ant-page-header-heading-title');
70 | expect(titleDom.text()).toEqual('name');
71 | });
72 | });
73 |
--------------------------------------------------------------------------------
/example/src/pages/user/login/style.less:
--------------------------------------------------------------------------------
1 | @import '~antd/es/style/themes/default.less';
2 |
3 | .container {
4 | display: flex;
5 | flex-direction: column;
6 | height: 100vh;
7 | overflow: auto;
8 | background: @layout-body-background;
9 | }
10 |
11 | .lang {
12 | width: 100%;
13 | height: 40px;
14 | line-height: 44px;
15 | text-align: right;
16 | :global(.ant-dropdown-trigger) {
17 | margin-right: 24px;
18 | }
19 | }
20 |
21 | .content {
22 | flex: 1;
23 | padding: 32px 0;
24 | }
25 |
26 | @media (min-width: @screen-md-min) {
27 | .container {
28 | background-image: url('https://gw.alipayobjects.com/zos/rmsportal/TVYTbAXWheQpRcWDaDMu.svg');
29 | background-repeat: no-repeat;
30 | background-position: center 110px;
31 | background-size: 100%;
32 | }
33 |
34 | .content {
35 | padding: 32px 0 24px;
36 | }
37 | }
38 |
39 | .top {
40 | text-align: center;
41 | }
42 |
43 | .header {
44 | height: 44px;
45 | line-height: 44px;
46 | a {
47 | text-decoration: none;
48 | }
49 | }
50 |
51 | .logo {
52 | height: 44px;
53 | margin-right: 16px;
54 | vertical-align: top;
55 | }
56 |
57 | .title {
58 | position: relative;
59 | top: 2px;
60 | color: @heading-color;
61 | font-weight: 600;
62 | font-size: 33px;
63 | font-family: Avenir, 'Helvetica Neue', Arial, Helvetica, sans-serif;
64 | }
65 |
66 | .desc {
67 | margin-top: 12px;
68 | margin-bottom: 40px;
69 | color: @text-color-secondary;
70 | font-size: @font-size-base;
71 | }
72 |
73 | .main {
74 | width: 368px;
75 | margin: 0 auto;
76 | @media screen and (max-width: @screen-sm) {
77 | width: 95%;
78 | }
79 |
80 | .icon {
81 | margin-left: 16px;
82 | color: rgba(0, 0, 0, 0.2);
83 | font-size: 24px;
84 | vertical-align: middle;
85 | cursor: pointer;
86 | transition: color 0.3s;
87 |
88 | &:hover {
89 | color: @primary-color;
90 | }
91 | }
92 |
93 | .other {
94 | margin-top: 24px;
95 | line-height: 22px;
96 | text-align: left;
97 |
98 | .register {
99 | float: right;
100 | }
101 | }
102 |
103 | :global {
104 | .antd-pro-login-submit {
105 | width: 100%;
106 | margin-top: 24px;
107 | }
108 | }
109 | }
110 |
--------------------------------------------------------------------------------
/docs/index.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: 介绍
3 | order: 10
4 | side: false
5 | hero:
6 | title: ProLayout
7 | desc: 🏆 Use Ant Design Table like a Pro!
8 | actions:
9 | - text: 快速开始 →
10 | link: /getting-started
11 | features:
12 | - icon: https://gw.alipayobjects.com/os/q/cms/images/k9ziitmp/13668549-b393-42a2-97c3-a6365ba87ac2_w96_h96.png
13 | title: 简单易用
14 | desc: 开箱即用的 Layout 组件,一步即可生成layout
15 | - icon: https://gw.alipayobjects.com/os/q/cms/images/k9ziik0f/487a2685-8f68-4c34-824f-e34c171d0dfd_w96_h96.png
16 | title: Ant Design
17 | desc: 与 Ant Design 设计体系一脉相承,无缝对接 antd 项目,兼容 antd 3.x & 4.x
18 | - icon: https://gw.alipayobjects.com/os/q/cms/images/k9ziip85/89434dcf-5f1d-4362-9ce0-ab8012a85924_w96_h96.png
19 | title: 国际化
20 | desc: 提供完备的国际化语言支持,与 Ant Design 体系打通
21 | - icon: https://gw.alipayobjects.com/mdn/rms_05efff/afts/img/A*-3XMTrwP85wAAAAAAAAAAABkARQnAQ
22 | title: 预设样式
23 | desc: 样式风格与 antd 一脉相承,无需魔改,浑然天成
24 | - icon: https://gw.alipayobjects.com/os/q/cms/images/k9ziieuq/decadf3f-b53a-4c48-83f3-a2faaccf9ff7_w96_h96.png
25 | title: 预设行为
26 | desc: 路由可以默认的生成菜单和面包屑, 并且自动更新浏览器的 title
27 | - icon: https://gw.alipayobjects.com/os/q/cms/images/k9zij2bh/67f75d56-0d62-47d6-a8a5-dbd0cb79a401_w96_h96.png
28 | title: Typescript
29 | desc: 使用 TypeScript 开发,提供完整的类型定义文件
30 |
31 | footer: Open-source MIT Licensed | Copyright © 2017-present
32 | ---
33 |
34 | ## 使用
35 |
36 | ```bash
37 | npm i @ant-design/pro-layout --save
38 | // or
39 | yarn add @ant-design/pro-layout
40 | ```
41 |
42 | ```jsx | pure
43 | import BasicLayout from '@ant-design/pro-layout';
44 |
45 | render(, document.getElementById('root'));
46 | ```
47 |
48 | ## 示例
49 |
50 | [site](https://ant-design.github.io/ant-design-pro-layout/)
51 |
52 | # 基本使用
53 |
54 | ProLayout 与 umi 配合使用会有最好的效果,umi 会把 config.ts 中的路由帮我们自动注入到配置的 layout 中,这样我们就可以免去手写菜单的烦恼。
55 |
56 | ProLayout 扩展了 umi 的 router 配置,新增了 name,icon,locale,hideInMenu,hideChildrenInMenu 等配置,这样可以更方便的生成菜单,在一个地方配置即可。数据格式如下:
57 |
58 | ```ts | pure
59 | export interface MenuDataItem {
60 | hideChildrenInMenu?: boolean;
61 | hideInMenu?: boolean;
62 | icon?: string;
63 | locale?: string;
64 | name?: string;
65 | path: string;
66 | [key: string]: any;
67 | }
68 | ```
69 |
70 | ProLayout 会根据 `location.pathname` 来自动选中菜单,并且自动生成相应的面包屑。如果不想使用可以自己配置 `selectedKeys` 和 `openKeys` 来进行受控配置。
71 |
72 | ## Demo
73 |
74 |
75 |
--------------------------------------------------------------------------------
/src/FooterToolbar/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useContext, useEffect, useMemo, ReactNode } from 'react';
2 | import { Space } from 'antd';
3 | import classNames from 'classnames';
4 |
5 | import './index.less';
6 | import { RouteContext, RouteContextType } from '../index';
7 |
8 | export interface FooterToolbarProps {
9 | extra?: React.ReactNode;
10 | style?: React.CSSProperties;
11 | className?: string;
12 | renderContent?: (
13 | props: FooterToolbarProps & RouteContextType & { leftWidth?: string },
14 | dom: JSX.Element,
15 | ) => ReactNode;
16 | prefixCls?: string;
17 | }
18 | const FooterToolbar: React.FC = (props) => {
19 | const {
20 | children,
21 | prefixCls = 'ant-pro',
22 | className,
23 | extra,
24 | renderContent,
25 | ...restProps
26 | } = props;
27 |
28 | const baseClassName = `${prefixCls}-footer-bar`;
29 | const value = useContext(RouteContext);
30 | const width = useMemo(() => {
31 | const { hasSiderMenu, isMobile, siderWidth } = value;
32 | if (!hasSiderMenu) {
33 | return undefined;
34 | }
35 | // 0 or undefined
36 | if (!siderWidth) {
37 | return '100%';
38 | }
39 | return isMobile ? '100%' : `calc(100% - ${siderWidth}px)`;
40 | }, [value.collapsed, value.hasSiderMenu, value.isMobile, value.siderWidth]);
41 |
42 | const dom = (
43 | <>
44 | {extra}
45 |
46 | {children}
47 |
48 | >
49 | );
50 |
51 | /**
52 | * 告诉 props 是否存在 footerBar
53 | */
54 | useEffect(() => {
55 | if (!value || !value?.setHasFooterToolbar) {
56 | return () => {};
57 | }
58 | value?.setHasFooterToolbar(true);
59 | return () => {
60 | if (!value || !value?.setHasFooterToolbar) {
61 | return;
62 | }
63 | value?.setHasFooterToolbar(false);
64 | };
65 | }, []);
66 |
67 | return (
68 |
73 | {renderContent
74 | ? renderContent(
75 | {
76 | ...props,
77 | ...value,
78 | leftWidth: width,
79 | },
80 | dom,
81 | )
82 | : dom}
83 |
84 | );
85 | };
86 |
87 | export default FooterToolbar;
88 |
--------------------------------------------------------------------------------
/example/src/components/NoticeIcon/NoticeList.less:
--------------------------------------------------------------------------------
1 | @import '~antd/es/style/themes/default.less';
2 |
3 | .list {
4 | max-height: 400px;
5 | overflow: auto;
6 | &::-webkit-scrollbar {
7 | display: none;
8 | }
9 | .item {
10 | padding-right: 24px;
11 | padding-left: 24px;
12 | overflow: hidden;
13 | cursor: pointer;
14 | transition: all 0.3s;
15 |
16 | .meta {
17 | width: 100%;
18 | }
19 |
20 | .avatar {
21 | margin-top: 4px;
22 | background: @component-background;
23 | }
24 | .iconElement {
25 | font-size: 32px;
26 | }
27 |
28 | &.read {
29 | opacity: 0.4;
30 | }
31 | &:last-child {
32 | border-bottom: 0;
33 | }
34 | &:hover {
35 | background: @primary-1;
36 | }
37 | .title {
38 | margin-bottom: 8px;
39 | font-weight: normal;
40 | }
41 | .description {
42 | font-size: 12px;
43 | line-height: @line-height-base;
44 | }
45 | .datetime {
46 | margin-top: 4px;
47 | font-size: 12px;
48 | line-height: @line-height-base;
49 | }
50 | .extra {
51 | float: right;
52 | margin-top: -1.5px;
53 | margin-right: 0;
54 | color: @text-color-secondary;
55 | font-weight: normal;
56 | }
57 | }
58 | .loadMore {
59 | padding: 8px 0;
60 | color: @primary-6;
61 | text-align: center;
62 | cursor: pointer;
63 | &.loadedAll {
64 | color: rgba(0, 0, 0, 0.25);
65 | cursor: unset;
66 | }
67 | }
68 | }
69 |
70 | .notFound {
71 | padding: 73px 0 88px;
72 | color: @text-color-secondary;
73 | text-align: center;
74 | img {
75 | display: inline-block;
76 | height: 76px;
77 | margin-bottom: 16px;
78 | }
79 | }
80 |
81 | .bottomBar {
82 | height: 46px;
83 | color: @text-color;
84 | line-height: 46px;
85 | text-align: center;
86 | border-top: 1px solid @border-color-split;
87 | border-radius: 0 0 @border-radius-base @border-radius-base;
88 | transition: all 0.3s;
89 | div {
90 | display: inline-block;
91 | width: 50%;
92 | cursor: pointer;
93 | transition: all 0.3s;
94 | user-select: none;
95 |
96 | &:only-child {
97 | width: 100%;
98 | }
99 | &:not(:only-child):last-child {
100 | border-left: 1px solid @border-color-split;
101 | }
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/src/SiderMenu/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from 'react';
2 | import { Drawer } from 'antd';
3 | import classNames from 'classnames';
4 | import Omit from 'omit.js';
5 | import { getFlatMenus } from '@umijs/route-utils';
6 |
7 | import { useDeepCompareEffect } from '../utils/utils';
8 | import SiderMenu, { SiderMenuProps } from './SiderMenu';
9 | import MenuCounter from './Counter';
10 |
11 | const SiderMenuWrapper: React.FC = (props) => {
12 | const {
13 | isMobile,
14 | menuData,
15 | siderWidth,
16 | collapsed,
17 | onCollapse,
18 | style,
19 | className,
20 | hide,
21 | prefixCls
22 | } = props;
23 | const { setFlatMenuKeys } = MenuCounter.useContainer();
24 |
25 | useDeepCompareEffect(() => {
26 | if (!menuData || menuData.length < 1) {
27 | return () => null;
28 | }
29 | // // 当 menu data 改变的时候重新计算这两个参数
30 | const newFlatMenus = getFlatMenus(menuData);
31 | const animationFrameId = requestAnimationFrame(() => {
32 | setFlatMenuKeys(Object.keys(newFlatMenus));
33 | });
34 | return () =>
35 | window.cancelAnimationFrame &&
36 | window.cancelAnimationFrame(animationFrameId);
37 | }, [menuData]);
38 |
39 | useEffect(() => {
40 | if (isMobile === true) {
41 | if (onCollapse) {
42 | onCollapse(true);
43 | }
44 | }
45 | }, [isMobile]);
46 |
47 | const omitProps = Omit(props, ['className', 'style']);
48 |
49 | if (hide) {
50 | return null;
51 | }
52 |
53 | return isMobile ? (
54 | onCollapse && onCollapse(true)}
59 | style={{
60 | padding: 0,
61 | height: '100vh',
62 | ...style,
63 | }}
64 | width={siderWidth}
65 | bodyStyle={{ height: '100vh', padding: 0 }}
66 | >
67 |
72 |
73 | ) : (
74 |
79 | );
80 | };
81 |
82 | SiderMenuWrapper.defaultProps = {
83 | onCollapse: () => undefined,
84 | };
85 |
86 | export default SiderMenuWrapper;
87 |
--------------------------------------------------------------------------------
/example/src/locales/en-US/menu.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | 'menu.welcome': 'Welcome',
3 | 'menu.more-blocks': 'More Blocks',
4 | 'menu.home': 'Home',
5 | 'menu.admin': 'Admin',
6 | 'menu.admin.sub-page': 'Subpage',
7 | 'menu.admin.sub-page2': 'Subpage 2',
8 | 'menu.admin.sub-page3': 'Subpage 3',
9 | 'menu.list.table-list.sub-page': 'Subpage',
10 | 'menu.list.table-list.sub-page2': 'Subpage 2',
11 | 'menu.list.table-list.sub-page3': 'Subpage 3',
12 | 'menu.login': 'Login',
13 | 'menu.register': 'Register',
14 | 'menu.register.result': 'Register Result',
15 | 'menu.dashboard': 'Dashboard',
16 | 'menu.dashboard.analysis': 'Analysis',
17 | 'menu.dashboard.monitor': 'Monitor',
18 | 'menu.dashboard.workplace': 'Workplace',
19 | 'menu.exception.403': '403',
20 | 'menu.exception.404': '404',
21 | 'menu.exception.500': '500',
22 | 'menu.form': 'Form',
23 | 'menu.form.basic-form': 'Basic Form',
24 | 'menu.form.step-form': 'Step Form',
25 | 'menu.form.step-form.info': 'Step Form(write transfer information)',
26 | 'menu.form.step-form.confirm': 'Step Form(confirm transfer information)',
27 | 'menu.form.step-form.result': 'Step Form(finished)',
28 | 'menu.form.advanced-form': 'Advanced Form',
29 | 'menu.list': 'List',
30 | 'menu.list.table-list': 'Search Table',
31 | 'menu.list.basic-list': 'Basic List',
32 | 'menu.list.card-list': 'Card List',
33 | 'menu.list.search-list': 'Search List',
34 | 'menu.list.search-list.articles': 'Search List(articles)',
35 | 'menu.list.search-list.projects': 'Search List(projects)',
36 | 'menu.list.search-list.applications': 'Search List(applications)',
37 | 'menu.profile': 'Profile',
38 | 'menu.profile.basic': 'Basic Profile',
39 | 'menu.profile.advanced': 'Advanced Profile',
40 | 'menu.result': 'Result',
41 | 'menu.result.success': 'Success',
42 | 'menu.result.fail': 'Fail',
43 | 'menu.exception': 'Exception',
44 | 'menu.exception.not-permission': '403',
45 | 'menu.exception.not-find': '404',
46 | 'menu.exception.server-error': '500',
47 | 'menu.exception.trigger': 'Trigger',
48 | 'menu.account': 'Account',
49 | 'menu.account.center': 'Account Center',
50 | 'menu.account.settings': 'Account Settings',
51 | 'menu.account.trigger': 'Trigger Error',
52 | 'menu.account.logout': 'Logout',
53 | 'menu.editor': 'Graphic Editor',
54 | 'menu.editor.flow': 'Flow Editor',
55 | 'menu.editor.mind': 'Mind Editor',
56 | 'menu.editor.koni': 'Koni Editor',
57 | };
58 |
--------------------------------------------------------------------------------
/example/src/components/RightContent/index.tsx:
--------------------------------------------------------------------------------
1 | import { Tooltip, Tag, Space } from 'antd';
2 | import { QuestionCircleOutlined } from '@ant-design/icons';
3 | import React from 'react';
4 | import { useModel, SelectLang } from 'umi';
5 | import Avatar from './AvatarDropdown';
6 | import HeaderSearch from '../HeaderSearch';
7 | import styles from './index.less';
8 | import { useIntl } from 'umi';
9 |
10 | export type SiderTheme = 'light' | 'dark';
11 |
12 | const ENVTagColor = {
13 | dev: 'orange',
14 | test: 'green',
15 | pre: '#87d068',
16 | };
17 |
18 | const GlobalHeaderRight: React.FC<{}> = () => {
19 | const { initialState } = useModel('@@initialState');
20 |
21 | if (!initialState || !initialState.settings) {
22 | return null;
23 | }
24 |
25 | const { navTheme, layout } = initialState.settings;
26 | let className = styles.right;
27 |
28 | if ((navTheme === 'dark' && layout === 'top') || layout === 'mix') {
29 | className = `${styles.right} ${styles.dark}`;
30 | }
31 | return (
32 |
33 | umi ui, value: 'umi ui' },
39 | {
40 | label: Ant Design,
41 | value: 'Ant Design',
42 | },
43 | {
44 | label: Pro Table,
45 | value: 'Pro Table',
46 | },
47 | {
48 | label: Pro Layout,
49 | value: 'Pro Layout',
50 | },
51 | ]}
52 | // onSearch={value => {
53 | // //console.log('input', value);
54 | // }}
55 | />
56 |
57 | {
60 | window.location.href = 'https://pro.ant.design/docs/getting-started';
61 | }}
62 | >
63 |
64 |
65 |
66 |
67 | {REACT_APP_ENV && (
68 |
69 | {REACT_APP_ENV}
70 |
71 | )}
72 |
73 |
74 | );
75 | };
76 | export default GlobalHeaderRight;
77 |
--------------------------------------------------------------------------------
/example/src/locales/pt-BR/menu.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | 'menu.welcome': 'Bem-vinda',
3 | 'menu.more-blocks': 'Mais blocos',
4 | 'menu.home': 'Início',
5 | 'menu.admin': 'Admin',
6 | 'menu.admin.sub-page': 'Subpágina',
7 | 'menu.admin.sub-page2': 'Subpágina 2',
8 | 'menu.admin.sub-page3': 'Subpágina 3',
9 | 'menu.list.table-list.sub-page': 'Subpágina',
10 | 'menu.list.table-list.sub-page2': 'Subpágina 2',
11 | 'menu.list.table-list.sub-page3': 'Subpágina 3',
12 | 'menu.login': 'Login',
13 | 'menu.register': 'Registro',
14 | 'menu.register.result': 'Resultado de registro',
15 | 'menu.dashboard': 'Dashboard',
16 | 'menu.dashboard.analysis': 'Análise',
17 | 'menu.dashboard.monitor': 'Monitor',
18 | 'menu.dashboard.workplace': 'Ambiente de Trabalho',
19 | 'menu.exception.403': '403',
20 | 'menu.exception.404': '404',
21 | 'menu.exception.500': '500',
22 | 'menu.form': 'Formulário',
23 | 'menu.form.basic-form': 'Formulário Básico',
24 | 'menu.form.step-form': 'Formulário Assistido',
25 | 'menu.form.step-form.info': 'Formulário Assistido(gravar informações de transferência)',
26 | 'menu.form.step-form.confirm': 'Formulário Assistido(confirmar informações de transferência)',
27 | 'menu.form.step-form.result': 'Formulário Assistido(finalizado)',
28 | 'menu.form.advanced-form': 'Formulário Avançado',
29 | 'menu.list': 'Lista',
30 | 'menu.list.table-list': 'Tabela de Busca',
31 | 'menu.list.basic-list': 'Lista Básica',
32 | 'menu.list.card-list': 'Lista de Card',
33 | 'menu.list.search-list': 'Lista de Busca',
34 | 'menu.list.search-list.articles': 'Lista de Busca(artigos)',
35 | 'menu.list.search-list.projects': 'Lista de Busca(projetos)',
36 | 'menu.list.search-list.applications': 'Lista de Busca(aplicações)',
37 | 'menu.profile': 'Perfil',
38 | 'menu.profile.basic': 'Perfil Básico',
39 | 'menu.profile.advanced': 'Perfil Avançado',
40 | 'menu.result': 'Resultado',
41 | 'menu.result.success': 'Sucesso',
42 | 'menu.result.fail': 'Falha',
43 | 'menu.exception': 'Exceção',
44 | 'menu.exception.not-permission': '403',
45 | 'menu.exception.not-find': '404',
46 | 'menu.exception.server-error': '500',
47 | 'menu.exception.trigger': 'Disparar',
48 | 'menu.account': 'Conta',
49 | 'menu.account.center': 'Central da Conta',
50 | 'menu.account.settings': 'Configurar Conta',
51 | 'menu.account.trigger': 'Disparar Erro',
52 | 'menu.account.logout': 'Sair',
53 | 'menu.editor': 'Graphic Editor',
54 | 'menu.editor.flow': 'Flow Editor',
55 | 'menu.editor.mind': 'Mind Editor',
56 | 'menu.editor.koni': 'Koni Editor',
57 | };
58 |
--------------------------------------------------------------------------------
/docs/menu.md:
--------------------------------------------------------------------------------
1 | ---
2 | title: 菜单能力
3 | order: 9
4 | side: false
5 | nav:
6 | title: 菜单能力
7 | order: 1
8 | ---
9 |
10 | # menu 的各种操作
11 |
12 | ProLayout 提供了强大的 menu,但是这样必然会封装很多行为,导致需要一些特殊逻辑的用户感到不满。所以我们提供了很多的 API,期望可以满足绝大部分客户的方式。
13 |
14 | ## 从服务器加载 menu
15 |
16 | 从服务器加载 menu 主要使用的 API 是 `menuDataRender` 和 `menuRender`,`menuDataRender`可以控制当前的菜单数据,`menuRender`可以控制菜单的 dom 节点。
17 |
18 |
19 |
20 | ## 从服务器加载 menu 并且使用 icon
21 |
22 | 这里主要是一个演示,我们需要准备一个枚举来进行 icon 的渲染,可以显著的减少打包的大小
23 |
24 |
25 |
26 | ## 从服务器加载 menu 并且使用旧版本 icon
27 |
28 | 使用兼容包来实现,虽然比较简单,但是会造成打包太大
29 |
30 |
31 |
32 | ## 自定义 menu 的内容
33 |
34 | 通过 `menuItemRender`, `subMenuItemRender`,`title`,`logo`,`menuHeaderRender` 可以非常方便的自定义 menu 的样式。如果实在是不满意,可以使用 `menuRender` 完全的自定义。
35 |
36 |
37 |
38 | ## 我是高手,我喜欢混着用
39 |
40 |
41 |
42 | ## 关闭时完全收起 menu
43 |
44 |
45 |
46 | ## 相关 API 展示
47 |
48 | | 参数 | 说明 | 类型 | 默认值 |
49 | | --- | --- | --- | --- |
50 | | title | layout 的 左上角 的 title | ReactNode | `'Ant Design Pro'` |
51 | | logo | layout 的 左上角 logo 的 url | ReactNode \| ()=>ReactNode | - |
52 | | loading | layout 的加载态 | boolean | - |
53 | | menuHeaderRender | 渲染 logo 和 title | ReactNode \| (logo,title)=>ReactNode | - |
54 | | menuRender | 自定义菜单的 render 方法 | (props: HeaderViewProps) => ReactNode | - |
55 | | layout | layout 的菜单模式,side:右侧导航,top:顶部导航 | 'side' \| 'top' | `'side'` |
56 | | breakpoint | 触发响应式布局的[断点](https://ant.design/components/grid-cn/#Col) | `Enum { 'xs', 'sm', 'md', 'lg', 'xl', 'xxl' }` | `lg` |
57 | | menuItemRender | 自定义菜单项的 render 方法 | (itemProps: MenuDataItem) => ReactNode | - |
58 | | subMenuItemRender | 自定义拥有子菜单菜单项的 render 方法 | (itemProps: MenuDataItem) => ReactNode | - |
59 | | menu | 关于 menu 的配置,暂时只有 locale,locale 可以关闭 menu 的自带的全球化 | { locale: boolean, defaultOpenAll: boolean } | `{ locale: true }` |
60 | | iconfontUrl | 使用 [IconFont](https://ant.design/components/icon-cn/#components-icon-demo-iconfont) 的图标配置 | string | - |
61 | | siderWidth | 侧边菜单宽度 | number | 256 |
62 | | collapsed | 控制菜单的收起和展开 | boolean | true |
63 | | onCollapse | 菜单的折叠收起事件 | (collapsed: boolean) => void | - |
64 | | disableMobile | 禁止自动切换到移动页面 | boolean | false |
65 | | links | 显示在菜单右下角的快捷操作 | ReactNode[] | - |
66 | | menuProps | 传递到 antd menu 组件的 props, 参考 (https://ant.design/components/menu-cn/) | MenuProps | undefined |
67 |
68 | 在 4.5.13 以后 Layout 通过 `menuProps` 支持 [Menu](https://ant.design/components/menu-cn/#Menu) 的大部分 props。
69 |
--------------------------------------------------------------------------------
/src/PageContainer/index.less:
--------------------------------------------------------------------------------
1 | @import '~antd/es/style/themes/default.less';
2 |
3 | @pro-layout-page-container: ~'@{ant-prefix}-pro-page-container';
4 |
5 | .@{pro-layout-page-container}-children-content {
6 | margin: 24px 24px 0;
7 | }
8 |
9 | .@{pro-layout-page-container} {
10 | &-warp {
11 | background-color: @component-background;
12 | .@{ant-prefix}-tabs-nav {
13 | margin: 0;
14 | }
15 | }
16 | &-ghost {
17 | .@{pro-layout-page-container}-warp {
18 | background-color: transparent;
19 | }
20 | }
21 | }
22 |
23 | .@{pro-layout-page-container}-main {
24 | .@{pro-layout-page-container}-detail {
25 | display: flex;
26 | }
27 |
28 | .@{pro-layout-page-container}-row {
29 | display: flex;
30 | width: 100%;
31 | }
32 |
33 | .@{pro-layout-page-container}-title-content {
34 | margin-bottom: 16px;
35 | }
36 |
37 | .@{pro-layout-page-container}-title,
38 | .@{pro-layout-page-container}-content {
39 | flex: auto;
40 | }
41 |
42 | .@{pro-layout-page-container}-extraContent,
43 | .@{pro-layout-page-container}-main {
44 | flex: 0 1 auto;
45 | }
46 |
47 | .@{pro-layout-page-container}-main {
48 | width: 100%;
49 | }
50 |
51 | .@{pro-layout-page-container}-title {
52 | margin-bottom: 16px;
53 | }
54 |
55 | .@{pro-layout-page-container}-logo {
56 | margin-bottom: 16px;
57 | }
58 |
59 | .@{pro-layout-page-container}-extraContent {
60 | min-width: 242px;
61 | margin-left: 88px;
62 | text-align: right;
63 | }
64 | }
65 |
66 | @media screen and (max-width: @screen-xl) {
67 | .@{pro-layout-page-container}-main {
68 | .@{pro-layout-page-container}-extraContent {
69 | margin-left: 44px;
70 | }
71 | }
72 | }
73 |
74 | @media screen and (max-width: @screen-lg) {
75 | .@{pro-layout-page-container}-main {
76 | .@{pro-layout-page-container}-extraContent {
77 | margin-left: 20px;
78 | }
79 | }
80 | }
81 |
82 | @media screen and (max-width: @screen-md) {
83 | .@{pro-layout-page-container}-main {
84 | .@{pro-layout-page-container}-row {
85 | display: block;
86 | }
87 |
88 | .@{pro-layout-page-container}-action,
89 | .@{pro-layout-page-container}-extraContent {
90 | margin-left: 0;
91 | text-align: left;
92 | }
93 | }
94 | }
95 |
96 | @media screen and (max-width: @screen-sm) {
97 | .@{pro-layout-page-container}-detail {
98 | display: block;
99 | }
100 | .@{pro-layout-page-container}-extraContent {
101 | margin-left: 0;
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/example/src/locales/zh-CN/settings.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | 'app.settings.menuMap.basic': '基本设置',
3 | 'app.settings.menuMap.security': '安全设置',
4 | 'app.settings.menuMap.binding': '账号绑定',
5 | 'app.settings.menuMap.notification': '新消息通知',
6 | 'app.settings.basic.avatar': '头像',
7 | 'app.settings.basic.change-avatar': '更换头像',
8 | 'app.settings.basic.email': '邮箱',
9 | 'app.settings.basic.email-message': '请输入您的邮箱!',
10 | 'app.settings.basic.nickname': '昵称',
11 | 'app.settings.basic.nickname-message': '请输入您的昵称!',
12 | 'app.settings.basic.profile': '个人简介',
13 | 'app.settings.basic.profile-message': '请输入个人简介!',
14 | 'app.settings.basic.profile-placeholder': '个人简介',
15 | 'app.settings.basic.country': '国家/地区',
16 | 'app.settings.basic.country-message': '请输入您的国家或地区!',
17 | 'app.settings.basic.geographic': '所在省市',
18 | 'app.settings.basic.geographic-message': '请输入您的所在省市!',
19 | 'app.settings.basic.address': '街道地址',
20 | 'app.settings.basic.address-message': '请输入您的街道地址!',
21 | 'app.settings.basic.phone': '联系电话',
22 | 'app.settings.basic.phone-message': '请输入您的联系电话!',
23 | 'app.settings.basic.update': '更新基本信息',
24 | 'app.settings.security.strong': '强',
25 | 'app.settings.security.medium': '中',
26 | 'app.settings.security.weak': '弱',
27 | 'app.settings.security.password': '账户密码',
28 | 'app.settings.security.password-description': '当前密码强度',
29 | 'app.settings.security.phone': '密保手机',
30 | 'app.settings.security.phone-description': '已绑定手机',
31 | 'app.settings.security.question': '密保问题',
32 | 'app.settings.security.question-description': '未设置密保问题,密保问题可有效保护账户安全',
33 | 'app.settings.security.email': '备用邮箱',
34 | 'app.settings.security.email-description': '已绑定邮箱',
35 | 'app.settings.security.mfa': 'MFA 设备',
36 | 'app.settings.security.mfa-description': '未绑定 MFA 设备,绑定后,可以进行二次确认',
37 | 'app.settings.security.modify': '修改',
38 | 'app.settings.security.set': '设置',
39 | 'app.settings.security.bind': '绑定',
40 | 'app.settings.binding.taobao': '绑定淘宝',
41 | 'app.settings.binding.taobao-description': '当前未绑定淘宝账号',
42 | 'app.settings.binding.alipay': '绑定支付宝',
43 | 'app.settings.binding.alipay-description': '当前未绑定支付宝账号',
44 | 'app.settings.binding.dingding': '绑定钉钉',
45 | 'app.settings.binding.dingding-description': '当前未绑定钉钉账号',
46 | 'app.settings.binding.bind': '绑定',
47 | 'app.settings.notification.password': '账户密码',
48 | 'app.settings.notification.password-description': '其他用户的消息将以站内信的形式通知',
49 | 'app.settings.notification.messages': '系统消息',
50 | 'app.settings.notification.messages-description': '系统消息将以站内信的形式通知',
51 | 'app.settings.notification.todo': '待办任务',
52 | 'app.settings.notification.todo-description': '待办任务将以站内信的形式通知',
53 | 'app.settings.open': '开',
54 | 'app.settings.close': '关',
55 | };
56 |
--------------------------------------------------------------------------------
/example/src/locales/zh-TW/settings.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | 'app.settings.menuMap.basic': '基本設置',
3 | 'app.settings.menuMap.security': '安全設置',
4 | 'app.settings.menuMap.binding': '賬號綁定',
5 | 'app.settings.menuMap.notification': '新消息通知',
6 | 'app.settings.basic.avatar': '頭像',
7 | 'app.settings.basic.change-avatar': '更換頭像',
8 | 'app.settings.basic.email': '郵箱',
9 | 'app.settings.basic.email-message': '請輸入您的郵箱!',
10 | 'app.settings.basic.nickname': '昵稱',
11 | 'app.settings.basic.nickname-message': '請輸入您的昵稱!',
12 | 'app.settings.basic.profile': '個人簡介',
13 | 'app.settings.basic.profile-message': '請輸入個人簡介!',
14 | 'app.settings.basic.profile-placeholder': '個人簡介',
15 | 'app.settings.basic.country': '國家/地區',
16 | 'app.settings.basic.country-message': '請輸入您的國家或地區!',
17 | 'app.settings.basic.geographic': '所在省市',
18 | 'app.settings.basic.geographic-message': '請輸入您的所在省市!',
19 | 'app.settings.basic.address': '街道地址',
20 | 'app.settings.basic.address-message': '請輸入您的街道地址!',
21 | 'app.settings.basic.phone': '聯系電話',
22 | 'app.settings.basic.phone-message': '請輸入您的聯系電話!',
23 | 'app.settings.basic.update': '更新基本信息',
24 | 'app.settings.security.strong': '強',
25 | 'app.settings.security.medium': '中',
26 | 'app.settings.security.weak': '弱',
27 | 'app.settings.security.password': '賬戶密碼',
28 | 'app.settings.security.password-description': '當前密碼強度',
29 | 'app.settings.security.phone': '密保手機',
30 | 'app.settings.security.phone-description': '已綁定手機',
31 | 'app.settings.security.question': '密保問題',
32 | 'app.settings.security.question-description': '未設置密保問題,密保問題可有效保護賬戶安全',
33 | 'app.settings.security.email': '備用郵箱',
34 | 'app.settings.security.email-description': '已綁定郵箱',
35 | 'app.settings.security.mfa': 'MFA 設備',
36 | 'app.settings.security.mfa-description': '未綁定 MFA 設備,綁定後,可以進行二次確認',
37 | 'app.settings.security.modify': '修改',
38 | 'app.settings.security.set': '設置',
39 | 'app.settings.security.bind': '綁定',
40 | 'app.settings.binding.taobao': '綁定淘寶',
41 | 'app.settings.binding.taobao-description': '當前未綁定淘寶賬號',
42 | 'app.settings.binding.alipay': '綁定支付寶',
43 | 'app.settings.binding.alipay-description': '當前未綁定支付寶賬號',
44 | 'app.settings.binding.dingding': '綁定釘釘',
45 | 'app.settings.binding.dingding-description': '當前未綁定釘釘賬號',
46 | 'app.settings.binding.bind': '綁定',
47 | 'app.settings.notification.password': '賬戶密碼',
48 | 'app.settings.notification.password-description': '其他用戶的消息將以站內信的形式通知',
49 | 'app.settings.notification.messages': '系統消息',
50 | 'app.settings.notification.messages-description': '系統消息將以站內信的形式通知',
51 | 'app.settings.notification.todo': '待辦任務',
52 | 'app.settings.notification.todo-description': '待辦任務將以站內信的形式通知',
53 | 'app.settings.open': '開',
54 | 'app.settings.close': '關',
55 | };
56 |
--------------------------------------------------------------------------------
/example/src/components/HeaderSearch/index.tsx:
--------------------------------------------------------------------------------
1 | import { SearchOutlined } from '@ant-design/icons';
2 | import { AutoComplete, Input } from 'antd';
3 | import useMergeValue from 'use-merge-value';
4 | import { AutoCompleteProps } from 'antd/es/auto-complete';
5 | import React, { useRef } from 'react';
6 |
7 | import classNames from 'classnames';
8 | import styles from './index.less';
9 |
10 | export interface HeaderSearchProps {
11 | onSearch?: (value?: string) => void;
12 | onChange?: (value?: string) => void;
13 | onVisibleChange?: (b: boolean) => void;
14 | className?: string;
15 | placeholder?: string;
16 | options: AutoCompleteProps['options'];
17 | defaultOpen?: boolean;
18 | open?: boolean;
19 | defaultValue?: string;
20 | value?: string;
21 | }
22 |
23 | const HeaderSearch: React.FC = (props) => {
24 | const {
25 | className,
26 | defaultValue,
27 | onVisibleChange,
28 | placeholder,
29 | open,
30 | defaultOpen,
31 | ...restProps
32 | } = props;
33 |
34 | const inputRef = useRef(null);
35 |
36 | const [value, setValue] = useMergeValue(defaultValue, {
37 | value: props.value,
38 | onChange: props.onChange,
39 | });
40 |
41 | const [searchMode, setSearchMode] = useMergeValue(defaultOpen || false, {
42 | value: props.open,
43 | onChange: onVisibleChange,
44 | });
45 |
46 | const inputClass = classNames(styles.input, {
47 | [styles.show]: searchMode,
48 | });
49 |
50 | return (
51 | {
54 | setSearchMode(true);
55 | if (searchMode && inputRef.current) {
56 | inputRef.current.focus();
57 | }
58 | }}
59 | onTransitionEnd={({ propertyName }) => {
60 | if (propertyName === 'width' && !searchMode) {
61 | if (onVisibleChange) {
62 | onVisibleChange(searchMode);
63 | }
64 | }
65 | }}
66 | >
67 |
73 |
80 | {
87 | if (e.key === 'Enter') {
88 | if (restProps.onSearch) {
89 | restProps.onSearch(value);
90 | }
91 | }
92 | }}
93 | onBlur={() => {
94 | setSearchMode(false);
95 | }}
96 | />
97 |
98 |
99 | );
100 | };
101 |
102 | export default HeaderSearch;
103 |
--------------------------------------------------------------------------------
/tests/__tests__/settingDrawer.test.tsx:
--------------------------------------------------------------------------------
1 | import { mount, render } from 'enzyme';
2 | import React from 'react';
3 | import SettingDrawer, { SettingDrawerProps } from '../../src/SettingDrawer';
4 | import defaultSettings from './defaultSettings';
5 | import { waitForComponentToPaint } from './util';
6 |
7 | describe('settingDrawer.test', () => {
8 | beforeAll(() => {
9 | Object.defineProperty(window, 'matchMedia', {
10 | value: jest.fn(() => ({
11 | matches: false,
12 | addListener() {},
13 | removeListener() {},
14 | })),
15 | });
16 | Object.defineProperty(window, 'localStorage', {
17 | value: {
18 | getItem: jest.fn(() => 'zh-CN'),
19 | },
20 | });
21 | });
22 |
23 | it('base user', () => {
24 | const html = render(
25 | ,
30 | );
31 | expect(html).toMatchSnapshot();
32 | });
33 |
34 | it('settings = undefined', () => {
35 | const html = render(
36 | ,
41 | );
42 | expect(html).toMatchSnapshot();
43 | });
44 |
45 | it('hideColors = true', () => {
46 | const html = render(
47 | ,
53 | );
54 | expect(html).toMatchSnapshot();
55 | });
56 |
57 | it('hideHintAlert = true', () => {
58 | const html = render(
59 | ,
65 | );
66 | expect(html).toMatchSnapshot();
67 | });
68 |
69 | it('hideLoading = true', () => {
70 | const html = render(
71 | ,
77 | );
78 | expect(html).toMatchSnapshot();
79 | });
80 |
81 | it('hideCopyButton = true', () => {
82 | const html = render(
83 | ,
89 | );
90 | expect(html).toMatchSnapshot();
91 | });
92 |
93 | it('onCollapseChange', async () => {
94 | const onCollapseChange = jest.fn();
95 | const wrapper = mount(
96 | ,
102 | );
103 | await waitForComponentToPaint(wrapper);
104 | const button = wrapper.find('.ant-pro-setting-drawer-handle');
105 | button.simulate('click');
106 | expect(onCollapseChange).toHaveBeenCalled();
107 | });
108 | });
109 |
--------------------------------------------------------------------------------
/example/src/layouts/BasicLayout.tsx:
--------------------------------------------------------------------------------
1 | /**
2 | * Ant Design Pro v4 use `@ant-design/pro-layout` to handle Layout.
3 | * You can view component api by:
4 | * https://github.com/ant-design/ant-design-pro-layout
5 | */
6 |
7 | import ProLayout, {
8 | MenuDataItem,
9 | BasicLayoutProps as ProLayoutProps,
10 | SettingDrawer,
11 | } from '../../../src/';
12 | import { Select } from 'antd';
13 | import React from 'react';
14 | import { HeartTwoTone } from '@ant-design/icons';
15 | import defaultSettings from '../../config/defaultSettings';
16 | import Footer from '@/components/Footer';
17 | import { Link, history, useIntl, useModel } from 'umi';
18 | import RightContent from '@/components/RightContent';
19 |
20 | export interface BasicLayoutProps extends ProLayoutProps {
21 | breadcrumbNameMap: {
22 | [path: string]: MenuDataItem;
23 | };
24 | }
25 | export type BasicLayoutContext = { [K in 'location']: BasicLayoutProps[K] } & {
26 | breadcrumbNameMap: {
27 | [path: string]: MenuDataItem;
28 | };
29 | };
30 |
31 | const BasicLayout: React.FC = (props) => {
32 | const { initialState, setInitialState } = useModel('@@initialState');
33 | const { settings = defaultSettings } = initialState || {};
34 | const intl = useIntl();
35 | return (
36 | <>
37 |
41 |
42 | name
43 | ,
44 | ]}
45 | formatMessage={intl.formatMessage}
46 | menuItemRender={(menuItemProps, defaultDom) =>
47 | menuItemProps.isUrl ? (
48 | defaultDom
49 | ) : (
50 |
51 | {defaultDom}
52 |
53 | )
54 | }
55 | rightContentRender={() => }
56 | onMenuHeaderClick={() => history.push('/')}
57 | footerRender={() => }
58 | menuExtraRender={({ collapsed }) =>
59 | !collapsed &&
60 | props.location?.pathname === '/welcome' && (
61 |
69 | )
70 | }
71 | {...props}
72 | {...settings}
73 | menu={{
74 | defaultOpenAll: true,
75 | }}
76 | >
77 | {props.children}
78 |
79 |
82 | setInitialState({
83 | ...initialState,
84 | settings: config,
85 | })
86 | }
87 | />
88 | >
89 | );
90 | };
91 |
92 | export default BasicLayout;
93 |
--------------------------------------------------------------------------------
/example/src/components/RightContent/AvatarDropdown.tsx:
--------------------------------------------------------------------------------
1 | import React, { useCallback } from 'react';
2 | import { LogoutOutlined, SettingOutlined, UserOutlined } from '@ant-design/icons';
3 | import { Avatar, Menu, Spin } from 'antd';
4 | import { ClickParam } from 'antd/es/menu';
5 | import { history, useIntl, useModel } from 'umi';
6 | import { getPageQuery } from '@/utils/utils';
7 | import { outLogin } from '@/services/login';
8 |
9 | import { stringify } from 'querystring';
10 | import HeaderDropdown from '../HeaderDropdown';
11 | import styles from './index.less';
12 |
13 | export interface GlobalHeaderRightProps {
14 | menu?: boolean;
15 | }
16 |
17 | /**
18 | * 退出登录,并且将当前的 url 保存
19 | */
20 | const loginOut = async () => {
21 | await outLogin();
22 | const { redirect } = getPageQuery();
23 | // Note: There may be security issues, please note
24 | if (window.location.pathname !== '/user/login' && !redirect) {
25 | history.replace({
26 | pathname: '/user/login',
27 | search: stringify({
28 | redirect: window.location.href,
29 | }),
30 | });
31 | }
32 | };
33 |
34 | const AvatarDropdown: React.FC = ({ menu }) => {
35 | const { initialState, setInitialState } = useModel('@@initialState');
36 |
37 | const onMenuClick = useCallback((event: ClickParam) => {
38 | const { key } = event;
39 | if (key === 'logout') {
40 | setInitialState({ ...initialState, currentUser: undefined });
41 | loginOut();
42 | return;
43 | }
44 | history.push(`/account/${key}`);
45 | }, []);
46 |
47 | const loading = (
48 |
49 |
56 |
57 | );
58 |
59 | if (!initialState) {
60 | return loading;
61 | }
62 |
63 | const { currentUser } = initialState;
64 |
65 | if (!currentUser || !currentUser.name) {
66 | return loading;
67 | }
68 |
69 | const menuHeaderDropdown = (
70 |
90 | );
91 | return (
92 |
93 |
94 |
95 | {currentUser.name}
96 |
97 |
98 | );
99 | };
100 |
101 | export default AvatarDropdown;
102 |
--------------------------------------------------------------------------------
/docs/example/complexMenu.ts:
--------------------------------------------------------------------------------
1 | export default [
2 | {
3 | path: '/home',
4 | name: '首页',
5 | locale: 'menu.home',
6 | children: [
7 | {
8 | path: '/home/overview',
9 | name: '概述',
10 | hideInMenu: true,
11 | locale: 'menu.home.overview',
12 | },
13 | {
14 | path: '/home/search',
15 | name: '搜索',
16 | hideInMenu: true,
17 | locale: 'menu.home.search',
18 | },
19 | ],
20 | },
21 | {
22 | path: '/data_hui',
23 | name: '汇总数据',
24 | locale: 'menu.data_hui',
25 | children: [
26 | {
27 | collapsed: true,
28 | menuName: '域买家维度交易',
29 | name: '域买家维度交易',
30 | children: [
31 | {
32 | id: 2,
33 | name: '_交易_买家_月表',
34 | path:
35 | '/data_hui?tableName=adm_rk_cr_tb_trd_byr_ms&tableSchema=alifin_odps_birisk',
36 | },
37 | {
38 | name: '_航旅交易_买家_日表',
39 | path:
40 | '/data_hui?tableName=adm_rk_cr_tb_trv_byr_ds&tableSchema=alifin_odps_birisk',
41 | },
42 | ],
43 | },
44 | {
45 | name: '域买家维度交易2',
46 | path: '/',
47 | children: [
48 | {
49 | name: '_交易_买家_月表',
50 | path:
51 | '/data_hui?tableName=adm_rk_cr_tb_trd_byr_ms&tableSchema=alifin_odps_birisk',
52 | },
53 | {
54 | name: '_航旅交易_买家_日表',
55 | path:
56 | '/data_hui?tableName=adm_rk_cr_tb_trv_byr_ds&tableSchema=alifin_odps_birisk',
57 | },
58 | ],
59 | },
60 | {
61 | name: '域买家维度交易3',
62 | path: '/',
63 | children: [
64 | {
65 | name: '_交易_买家_月表2',
66 | path:
67 | '/data_hui?tableName=adm_rk_cr_tb_trd_byr_ms&tableSchema=alifin_odps_birisk',
68 | },
69 | {
70 | name: '_航旅交易_买家_日表3',
71 | path:
72 | '/data_hui?tableName=adm_rk_cr_tb_trv_byr_ds&tableSchema=alifin_odps_birisk',
73 | },
74 | ],
75 | },
76 | ],
77 | },
78 | {
79 | path: '/data_ming',
80 | name: '明细数据',
81 | locale: 'menu.data_ming',
82 | children: [
83 | {
84 | path: '/other/outLoadMenu',
85 | name: '菜单导出',
86 | locale: 'menu.other.outLoadMenu',
87 | hideInMenu: true,
88 | },
89 | {
90 | path: '/other/homeEdit',
91 | name: '概述导出',
92 | locale: 'menu.other.outHomeEdit',
93 | },
94 | ],
95 | },
96 | {
97 | path: '/other',
98 | name: '其他',
99 | locale: 'menu.other',
100 | children: [
101 | {
102 | path: '/other/upLoad',
103 | name: 'odps同步导入',
104 | locale: 'menu.other.upLoad',
105 | },
106 | {
107 | path: '/other/upLoadMenu',
108 | name: '菜单导入',
109 | locale: 'menu.other.upLoadMenu',
110 | },
111 | {
112 | path: '/other/homeEdit',
113 | name: '概述编辑',
114 | locale: 'menu.other.homeEdit',
115 | hideInMenu: true,
116 | },
117 | ],
118 | },
119 | ];
120 |
--------------------------------------------------------------------------------
/src/TopNavHeader/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useRef, useState } from 'react';
2 | import classNames from 'classnames';
3 | import ResizeObserver from 'rc-resize-observer';
4 |
5 | import {
6 | SiderMenuProps,
7 | defaultRenderLogoAndTitle,
8 | } from '../SiderMenu/SiderMenu';
9 | import './index.less';
10 |
11 | import BaseMenu from '../SiderMenu/BaseMenu';
12 | import { HeaderViewProps } from '../Header';
13 |
14 | export type TopNavHeaderProps = SiderMenuProps & {
15 | logo?: React.ReactNode;
16 | onCollapse?: (collapse: boolean) => void;
17 | rightContentRender?: HeaderViewProps['rightContentRender'];
18 | };
19 |
20 | /**
21 | * 抽离出来是为了防止 rightSize 经常改变导致菜单 render
22 | * @param param0
23 | */
24 | const RightContent: React.FC = ({
25 | rightContentRender,
26 | ...props
27 | }) => {
28 | const [rightSize, setRightSize] = useState('auto');
29 |
30 | return (
31 |
36 |
41 |
{
43 | if (!width) {
44 | return;
45 | }
46 | setRightSize(width);
47 | }}
48 | >
49 | {rightContentRender && (
50 |
51 | {rightContentRender({
52 | ...props,
53 | })}
54 |
55 | )}
56 |
57 |
58 |
59 | );
60 | };
61 |
62 | const TopNavHeader: React.FC = (props) => {
63 | const ref = useRef(null);
64 | const {
65 | theme,
66 | onMenuHeaderClick,
67 | contentWidth,
68 | rightContentRender,
69 | className: propsClassName,
70 | style,
71 | layout,
72 | } = props;
73 | const baseClassName = 'ant-pro-top-nav-header';
74 | const headerDom = defaultRenderLogoAndTitle(
75 | { ...props, collapsed: false },
76 | layout === 'mix' ? 'headerTitleRender' : undefined,
77 | );
78 |
79 | const className = classNames(baseClassName, propsClassName, {
80 | light: theme === 'light',
81 | });
82 | return (
83 |
84 |
90 | {headerDom && (
91 |
95 |
96 | {headerDom}
97 |
98 |
99 | )}
100 |
101 |
102 |
103 | {rightContentRender && (
104 |
105 | )}
106 |
107 |
108 | );
109 | };
110 |
111 | export default TopNavHeader;
112 |
--------------------------------------------------------------------------------
/example/mock/notices.ts:
--------------------------------------------------------------------------------
1 | import { Request, Response } from 'express';
2 |
3 | const getNotices = (req: Request, res: Response) => {
4 | res.json([
5 | {
6 | id: '000000001',
7 | avatar: 'https://gw.alipayobjects.com/zos/rmsportal/ThXAXghbEsBCCSDihZxY.png',
8 | title: '你收到了 14 份新周报',
9 | datetime: '2017-08-09',
10 | type: 'notification',
11 | },
12 | {
13 | id: '000000002',
14 | avatar: 'https://gw.alipayobjects.com/zos/rmsportal/OKJXDXrmkNshAMvwtvhu.png',
15 | title: '你推荐的 曲妮妮 已通过第三轮面试',
16 | datetime: '2017-08-08',
17 | type: 'notification',
18 | },
19 | {
20 | id: '000000003',
21 | avatar: 'https://gw.alipayobjects.com/zos/rmsportal/kISTdvpyTAhtGxpovNWd.png',
22 | title: '这种模板可以区分多种通知类型',
23 | datetime: '2017-08-07',
24 | read: true,
25 | type: 'notification',
26 | },
27 | {
28 | id: '000000004',
29 | avatar: 'https://gw.alipayobjects.com/zos/rmsportal/GvqBnKhFgObvnSGkDsje.png',
30 | title: '左侧图标用于区分不同的类型',
31 | datetime: '2017-08-07',
32 | type: 'notification',
33 | },
34 | {
35 | id: '000000005',
36 | avatar: 'https://gw.alipayobjects.com/zos/rmsportal/ThXAXghbEsBCCSDihZxY.png',
37 | title: '内容不要超过两行字,超出时自动截断',
38 | datetime: '2017-08-07',
39 | type: 'notification',
40 | },
41 | {
42 | id: '000000006',
43 | avatar: 'https://gw.alipayobjects.com/zos/rmsportal/fcHMVNCjPOsbUGdEduuv.jpeg',
44 | title: '曲丽丽 评论了你',
45 | description: '描述信息描述信息描述信息',
46 | datetime: '2017-08-07',
47 | type: 'message',
48 | clickClose: true,
49 | },
50 | {
51 | id: '000000007',
52 | avatar: 'https://gw.alipayobjects.com/zos/rmsportal/fcHMVNCjPOsbUGdEduuv.jpeg',
53 | title: '朱偏右 回复了你',
54 | description: '这种模板用于提醒谁与你发生了互动,左侧放『谁』的头像',
55 | datetime: '2017-08-07',
56 | type: 'message',
57 | clickClose: true,
58 | },
59 | {
60 | id: '000000008',
61 | avatar: 'https://gw.alipayobjects.com/zos/rmsportal/fcHMVNCjPOsbUGdEduuv.jpeg',
62 | title: '标题',
63 | description: '这种模板用于提醒谁与你发生了互动,左侧放『谁』的头像',
64 | datetime: '2017-08-07',
65 | type: 'message',
66 | clickClose: true,
67 | },
68 | {
69 | id: '000000009',
70 | title: '任务名称',
71 | description: '任务需要在 2017-01-12 20:00 前启动',
72 | extra: '未开始',
73 | status: 'todo',
74 | type: 'event',
75 | },
76 | {
77 | id: '000000010',
78 | title: '第三方紧急代码变更',
79 | description: '冠霖提交于 2017-01-06,需在 2017-01-07 前完成代码变更任务',
80 | extra: '马上到期',
81 | status: 'urgent',
82 | type: 'event',
83 | },
84 | {
85 | id: '000000011',
86 | title: '信息安全考试',
87 | description: '指派竹尔于 2017-01-09 前完成更新并发布',
88 | extra: '已耗时 8 天',
89 | status: 'doing',
90 | type: 'event',
91 | },
92 | {
93 | id: '000000012',
94 | title: 'ABCD 版本发布',
95 | description: '冠霖提交于 2017-01-06,需在 2017-01-07 前完成代码变更任务',
96 | extra: '进行中',
97 | status: 'processing',
98 | type: 'event',
99 | },
100 | ]);
101 | };
102 |
103 | export default {
104 | 'GET /api/notices': getNotices,
105 | };
106 |
--------------------------------------------------------------------------------
/docs/demo/base.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { Button, Descriptions, Result, Avatar } from 'antd';
3 | import { UserOutlined } from '@ant-design/icons';
4 |
5 | import ProLayout, {
6 | PageContainer,
7 | SettingDrawer,
8 | ProSettings,
9 | // eslint-disable-next-line import/no-unresolved
10 | } from '@ant-design/pro-layout';
11 | import defaultProps from './defaultProps';
12 |
13 | const content = (
14 |
15 | 张三
16 |
17 | 421421
18 |
19 | 2017-01-10
20 | 2017-10-10
21 |
22 | 中国浙江省杭州市西湖区古翠路
23 |
24 |
25 | );
26 |
27 | export default () => {
28 | const [settings, setSetting] = useState | undefined>(
29 | undefined,
30 | );
31 | const [pathname, setPathname] = useState('/welcome');
32 | return (
33 |
40 |
(
49 | {
51 | setPathname(item.path || '/welcome');
52 | }}
53 | >
54 | {dom}
55 |
56 | )}
57 | rightContentRender={() => (
58 |
61 | )}
62 | {...settings}
63 | >
64 | 操作,
78 | ,
79 | ,
82 | ]}
83 | footer={[, ]}
84 | >
85 |
90 | Back Home}
99 | />
100 |
101 |
102 |
103 |
document.getElementById('test-pro-layout')}
105 | settings={settings}
106 | onSettingChange={(changeSetting) => setSetting(changeSetting)}
107 | />
108 |
109 | );
110 | };
111 |
--------------------------------------------------------------------------------
/example/src/locales/en-US/settings.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | 'app.settings.menuMap.basic': 'Basic Settings',
3 | 'app.settings.menuMap.security': 'Security Settings',
4 | 'app.settings.menuMap.binding': 'Account Binding',
5 | 'app.settings.menuMap.notification': 'New Message Notification',
6 | 'app.settings.basic.avatar': 'Avatar',
7 | 'app.settings.basic.change-avatar': 'Change avatar',
8 | 'app.settings.basic.email': 'Email',
9 | 'app.settings.basic.email-message': 'Please input your email!',
10 | 'app.settings.basic.nickname': 'Nickname',
11 | 'app.settings.basic.nickname-message': 'Please input your Nickname!',
12 | 'app.settings.basic.profile': 'Personal profile',
13 | 'app.settings.basic.profile-message': 'Please input your personal profile!',
14 | 'app.settings.basic.profile-placeholder': 'Brief introduction to yourself',
15 | 'app.settings.basic.country': 'Country/Region',
16 | 'app.settings.basic.country-message': 'Please input your country!',
17 | 'app.settings.basic.geographic': 'Province or city',
18 | 'app.settings.basic.geographic-message': 'Please input your geographic info!',
19 | 'app.settings.basic.address': 'Street Address',
20 | 'app.settings.basic.address-message': 'Please input your address!',
21 | 'app.settings.basic.phone': 'Phone Number',
22 | 'app.settings.basic.phone-message': 'Please input your phone!',
23 | 'app.settings.basic.update': 'Update Information',
24 | 'app.settings.security.strong': 'Strong',
25 | 'app.settings.security.medium': 'Medium',
26 | 'app.settings.security.weak': 'Weak',
27 | 'app.settings.security.password': 'Account Password',
28 | 'app.settings.security.password-description': 'Current password strength',
29 | 'app.settings.security.phone': 'Security Phone',
30 | 'app.settings.security.phone-description': 'Bound phone',
31 | 'app.settings.security.question': 'Security Question',
32 | 'app.settings.security.question-description':
33 | 'The security question is not set, and the security policy can effectively protect the account security',
34 | 'app.settings.security.email': 'Backup Email',
35 | 'app.settings.security.email-description': 'Bound Email',
36 | 'app.settings.security.mfa': 'MFA Device',
37 | 'app.settings.security.mfa-description':
38 | 'Unbound MFA device, after binding, can be confirmed twice',
39 | 'app.settings.security.modify': 'Modify',
40 | 'app.settings.security.set': 'Set',
41 | 'app.settings.security.bind': 'Bind',
42 | 'app.settings.binding.taobao': 'Binding Taobao',
43 | 'app.settings.binding.taobao-description': 'Currently unbound Taobao account',
44 | 'app.settings.binding.alipay': 'Binding Alipay',
45 | 'app.settings.binding.alipay-description': 'Currently unbound Alipay account',
46 | 'app.settings.binding.dingding': 'Binding DingTalk',
47 | 'app.settings.binding.dingding-description': 'Currently unbound DingTalk account',
48 | 'app.settings.binding.bind': 'Bind',
49 | 'app.settings.notification.password': 'Account Password',
50 | 'app.settings.notification.password-description':
51 | 'Messages from other users will be notified in the form of a station letter',
52 | 'app.settings.notification.messages': 'System Messages',
53 | 'app.settings.notification.messages-description':
54 | 'System messages will be notified in the form of a station letter',
55 | 'app.settings.notification.todo': 'To-do Notification',
56 | 'app.settings.notification.todo-description':
57 | 'The to-do list will be notified in the form of a letter from the station',
58 | 'app.settings.open': 'Open',
59 | 'app.settings.close': 'Close',
60 | };
61 |
--------------------------------------------------------------------------------
/example/src/components/NoticeIcon/NoticeList.tsx:
--------------------------------------------------------------------------------
1 | import { Avatar, List } from 'antd';
2 |
3 | import React from 'react';
4 | import classNames from 'classnames';
5 | import { NoticeIconData } from './index';
6 | import styles from './NoticeList.less';
7 |
8 | export interface NoticeIconTabProps {
9 | count?: number;
10 | name?: string;
11 | showClear?: boolean;
12 | showViewMore?: boolean;
13 | style?: React.CSSProperties;
14 | title: string;
15 | tabKey: string;
16 | data?: NoticeIconData[];
17 | onClick?: (item: NoticeIconData) => void;
18 | onClear?: () => void;
19 | emptyText?: string;
20 | clearText?: string;
21 | viewMoreText?: string;
22 | list: NoticeIconData[];
23 | onViewMore?: (e: any) => void;
24 | }
25 | const NoticeList: React.SFC = ({
26 | data = [],
27 | onClick,
28 | onClear,
29 | title,
30 | onViewMore,
31 | emptyText,
32 | showClear = true,
33 | clearText,
34 | viewMoreText,
35 | showViewMore = false,
36 | }) => {
37 | if (!data || data.length === 0) {
38 | return (
39 |
40 |

44 |
{emptyText}
45 |
46 | );
47 | }
48 | return (
49 |
50 |
51 | className={styles.list}
52 | dataSource={data}
53 | renderItem={(item, i) => {
54 | const itemCls = classNames(styles.item, {
55 | [styles.read]: item.read,
56 | });
57 | // eslint-disable-next-line no-nested-ternary
58 | const leftIcon = item.avatar ? (
59 | typeof item.avatar === 'string' ? (
60 |
61 | ) : (
62 | {item.avatar}
63 | )
64 | ) : null;
65 |
66 | return (
67 | onClick && onClick(item)}
71 | >
72 |
77 | {item.title}
78 | {item.extra}
79 |
80 | }
81 | description={
82 |
83 |
{item.description}
84 |
{item.datetime}
85 |
86 | }
87 | />
88 |
89 | );
90 | }}
91 | />
92 |
93 | {showClear ? (
94 |
95 | {clearText} {title}
96 |
97 | ) : null}
98 | {showViewMore ? (
99 |
{
101 | if (onViewMore) {
102 | onViewMore(e);
103 | }
104 | }}
105 | >
106 | {viewMoreText}
107 |
108 | ) : null}
109 |
110 |
111 | );
112 | };
113 |
114 | export default NoticeList;
115 |
--------------------------------------------------------------------------------
/example/config/config.ts:
--------------------------------------------------------------------------------
1 | // https://umijs.org/config/
2 | import { defineConfig } from 'umi';
3 | import defaultSettings from './defaultSettings';
4 | import proxy from './proxy';
5 |
6 | const { REACT_APP_ENV } = process.env;
7 |
8 | export default defineConfig({
9 | hash: true,
10 | antd: {},
11 | dva: {
12 | hmr: true,
13 | },
14 | layout: false,
15 | locale: {
16 | // default zh-CN
17 | default: 'zh-CN',
18 | // default true, when it is true, will use `navigator.language` overwrite default
19 | antd: true,
20 | baseNavigator: true,
21 | },
22 | chainWebpack(memo) {
23 | memo.module.rule('ts-in-node_modules').include.clear();
24 | return memo;
25 | },
26 | history: {
27 | type: 'hash',
28 | },
29 | dynamicImport: false,
30 | targets: {
31 | ie: 11,
32 | },
33 | // umi routes: https://umijs.org/docs/routing
34 | routes: [
35 | {
36 | path: '/user',
37 | layout: false,
38 | routes: [
39 | {
40 | name: 'login',
41 | path: '/user/login',
42 | layout: false,
43 | component: './user/login',
44 | },
45 | ],
46 | },
47 | {
48 | path: '/',
49 | component: '../layouts/BasicLayout',
50 | routes: [
51 | {
52 | path: '/welcome',
53 | name: 'welcome',
54 | icon: 'smile',
55 | component: './Welcome',
56 | },
57 | {
58 | path: '/admin',
59 | name: 'admin',
60 | icon: 'crown',
61 | access: 'canAdmin',
62 | component: './Admin',
63 | routes: [
64 | {
65 | path: '/admin/sub-page',
66 | name: 'sub-page',
67 | icon: 'crown',
68 | component: './Welcome',
69 | },
70 | {
71 | path: '/admin/sub-page2',
72 | name: 'sub-page2',
73 | icon: 'crown',
74 | component: './Welcome',
75 | },
76 | {
77 | path: '/admin/sub-page3',
78 | name: 'sub-page3',
79 | icon: 'crown',
80 | component: './Welcome',
81 | },
82 | ],
83 | },
84 | {
85 | name: 'list.table-list',
86 | icon: 'table',
87 | path: '/list',
88 | component: './ListTableList',
89 | routes: [
90 | {
91 | path: '/list/sub-page',
92 | name: 'sub-page',
93 | icon: 'crown',
94 | component: './Welcome',
95 | },
96 | {
97 | path: '/list/sub-page2',
98 | name: 'sub-page2',
99 | icon: 'crown',
100 | component: './Welcome',
101 | },
102 | {
103 | path: '/list/sub-page3',
104 | name: 'sub-page3',
105 | icon: 'crown',
106 | component: './Welcome',
107 | },
108 | ],
109 | },
110 | {
111 | path: '/',
112 | redirect: '/welcome',
113 | },
114 | ],
115 | },
116 | {
117 | component: './404',
118 | },
119 | ],
120 | // Theme for antd: https://ant.design/docs/react/customize-theme-cn
121 | theme: {
122 | // ...darkTheme,
123 | 'primary-color': defaultSettings.primaryColor,
124 | },
125 | ignoreMomentLocale: true,
126 | proxy: proxy[REACT_APP_ENV || 'dev'],
127 | manifest: {
128 | basePath: '/',
129 | },
130 | });
131 |
--------------------------------------------------------------------------------
/docs/demo/api.tsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable import/no-unresolved */
2 | import React, { useState } from 'react';
3 | import { Switch, Avatar } from 'antd';
4 | import ProLayout, {
5 | PageContainer,
6 | DefaultFooter,
7 | } from '@ant-design/pro-layout';
8 | import defaultProps from './defaultProps';
9 |
10 | export default () => {
11 | const [loading, setLoading] = useState(false);
12 | const [collapsed, setCollapsed] = useState(true);
13 | const [menu, setMenu] = useState(true);
14 | const [header, setHeader] = useState(true);
15 | const [footer, setFooter] = useState(true);
16 | const [menuHeader, setMenuHeader] = useState(true);
17 | const [right, setRight] = useState(true);
18 | const [pure, setPure] = useState(false);
19 | const [collapsedButtonRender, setCollapsedButtonRender] = useState(true);
20 | return (
21 | <>
22 | setLoading(e)}
25 | style={{
26 | margin: 8,
27 | }}
28 | />
29 | loading 状态
30 | setCollapsed(e)}
33 | style={{
34 | margin: 8,
35 | }}
36 | />
37 | 折叠layout
38 | setMenu(e)}
41 | style={{
42 | margin: 8,
43 | }}
44 | />
45 | 显示菜单
46 | setCollapsedButtonRender(e)}
49 | style={{
50 | margin: 8,
51 | }}
52 | />
53 | 显示折叠按钮
54 | setHeader(e)}
57 | style={{
58 | margin: 8,
59 | }}
60 | />
61 | 显示顶栏
62 | setMenuHeader(e)}
65 | style={{
66 | margin: 8,
67 | }}
68 | />
69 | 显示菜单头
70 | setFooter(e)}
73 | style={{
74 | margin: 8,
75 | }}
76 | />
77 | 显示页脚
78 | setRight(e)}
81 | style={{
82 | margin: 8,
83 | }}
84 | />
85 | 显示顶栏右侧
86 | setPure(e)}
89 | style={{
90 | margin: 8,
91 | }}
92 | />
93 | 清爽模式
94 |
95 |
96 | (menu ? dom : null)}
105 | breakpoint={false}
106 | collapsed={collapsed}
107 | loading={loading}
108 | onCollapse={setCollapsed}
109 | rightContentRender={() =>
110 | right ? (
111 |
114 | ) : null
115 | }
116 | location={{
117 | pathname: '/welcome',
118 | }}
119 | pure={pure}
120 | footerRender={() => (footer ? : null)}
121 | >
122 | Hello World
123 |
124 | >
125 | );
126 | };
127 |
--------------------------------------------------------------------------------
/example/src/locales/pt-BR/settings.ts:
--------------------------------------------------------------------------------
1 | export default {
2 | 'app.settings.menuMap.basic': 'Configurações Básicas',
3 | 'app.settings.menuMap.security': 'Configurações de Segurança',
4 | 'app.settings.menuMap.binding': 'Vinculação de Conta',
5 | 'app.settings.menuMap.notification': 'Mensagens de Notificação',
6 | 'app.settings.basic.avatar': 'Avatar',
7 | 'app.settings.basic.change-avatar': 'Alterar avatar',
8 | 'app.settings.basic.email': 'Email',
9 | 'app.settings.basic.email-message': 'Por favor insira seu email!',
10 | 'app.settings.basic.nickname': 'Nome de usuário',
11 | 'app.settings.basic.nickname-message': 'Por favor insira seu nome de usuário!',
12 | 'app.settings.basic.profile': 'Perfil pessoal',
13 | 'app.settings.basic.profile-message': 'Por favor insira seu perfil pessoal!',
14 | 'app.settings.basic.profile-placeholder': 'Breve introdução sua',
15 | 'app.settings.basic.country': 'País/Região',
16 | 'app.settings.basic.country-message': 'Por favor insira país!',
17 | 'app.settings.basic.geographic': 'Província, estado ou cidade',
18 | 'app.settings.basic.geographic-message': 'Por favor insira suas informações geográficas!',
19 | 'app.settings.basic.address': 'Endereço',
20 | 'app.settings.basic.address-message': 'Por favor insira seu endereço!',
21 | 'app.settings.basic.phone': 'Número de telefone',
22 | 'app.settings.basic.phone-message': 'Por favor insira seu número de telefone!',
23 | 'app.settings.basic.update': 'Atualizar Informações',
24 | 'app.settings.security.strong': 'Forte',
25 | 'app.settings.security.medium': 'Média',
26 | 'app.settings.security.weak': 'Fraca',
27 | 'app.settings.security.password': 'Senha da Conta',
28 | 'app.settings.security.password-description': 'Força da senha',
29 | 'app.settings.security.phone': 'Telefone de Seguraça',
30 | 'app.settings.security.phone-description': 'Telefone vinculado',
31 | 'app.settings.security.question': 'Pergunta de Segurança',
32 | 'app.settings.security.question-description':
33 | 'A pergunta de segurança não está definida e a política de segurança pode proteger efetivamente a segurança da conta',
34 | 'app.settings.security.email': 'Email de Backup',
35 | 'app.settings.security.email-description': 'Email vinculado',
36 | 'app.settings.security.mfa': 'Dispositivo MFA',
37 | 'app.settings.security.mfa-description':
38 | 'O dispositivo MFA não vinculado, após a vinculação, pode ser confirmado duas vezes',
39 | 'app.settings.security.modify': 'Modificar',
40 | 'app.settings.security.set': 'Atribuir',
41 | 'app.settings.security.bind': 'Vincular',
42 | 'app.settings.binding.taobao': 'Vincular Taobao',
43 | 'app.settings.binding.taobao-description': 'Atualmente não vinculado à conta Taobao',
44 | 'app.settings.binding.alipay': 'Vincular Alipay',
45 | 'app.settings.binding.alipay-description': 'Atualmente não vinculado à conta Alipay',
46 | 'app.settings.binding.dingding': 'Vincular DingTalk',
47 | 'app.settings.binding.dingding-description': 'Atualmente não vinculado à conta DingTalk',
48 | 'app.settings.binding.bind': 'Vincular',
49 | 'app.settings.notification.password': 'Senha da Conta',
50 | 'app.settings.notification.password-description':
51 | 'Mensagens de outros usuários serão notificadas na forma de uma estação de letra',
52 | 'app.settings.notification.messages': 'Mensagens de Sistema',
53 | 'app.settings.notification.messages-description':
54 | 'Mensagens de sistema serão notificadas na forma de uma estação de letra',
55 | 'app.settings.notification.todo': 'Notificação de To-do',
56 | 'app.settings.notification.todo-description':
57 | 'A lista de to-do será notificada na forma de uma estação de letra',
58 | 'app.settings.open': 'Aberto',
59 | 'app.settings.close': 'Fechado',
60 | };
61 |
--------------------------------------------------------------------------------