├── public ├── CNAME └── favicon.ico ├── example ├── public │ ├── CNAME │ ├── favicon.png │ ├── home_bg.png │ └── icons │ │ ├── icon-128x128.png │ │ ├── icon-192x192.png │ │ └── icon-512x512.png ├── .eslintignore ├── .prettierrc.js ├── .stylelintrc.js ├── mock │ ├── route.ts │ └── notices.ts ├── src │ ├── locales │ │ ├── zh-CN │ │ │ ├── component.ts │ │ │ ├── pwa.ts │ │ │ ├── globalHeader.ts │ │ │ ├── settingDrawer.ts │ │ │ ├── menu.ts │ │ │ └── settings.ts │ │ ├── zh-TW │ │ │ ├── component.ts │ │ │ ├── pwa.ts │ │ │ ├── globalHeader.ts │ │ │ ├── settingDrawer.ts │ │ │ ├── menu.ts │ │ │ └── settings.ts │ │ ├── en-US │ │ │ ├── component.ts │ │ │ ├── pwa.ts │ │ │ ├── globalHeader.ts │ │ │ ├── settingDrawer.ts │ │ │ ├── menu.ts │ │ │ └── settings.ts │ │ ├── pt-BR │ │ │ ├── component.ts │ │ │ ├── pwa.ts │ │ │ ├── globalHeader.ts │ │ │ ├── settingDrawer.ts │ │ │ ├── menu.ts │ │ │ └── settings.ts │ │ ├── zh-TW.ts │ │ ├── pt-BR.ts │ │ ├── zh-CN.ts │ │ └── en-US.ts │ ├── pages │ │ ├── Welcome.less │ │ ├── user │ │ │ └── login │ │ │ │ ├── components │ │ │ │ └── Login │ │ │ │ │ ├── LoginContext.tsx │ │ │ │ │ ├── LoginSubmit.tsx │ │ │ │ │ ├── index.less │ │ │ │ │ ├── LoginTab.tsx │ │ │ │ │ └── map.tsx │ │ │ │ └── style.less │ │ ├── 404.tsx │ │ ├── ListTableList │ │ │ ├── components │ │ │ │ └── CreateForm.tsx │ │ │ └── data.d.ts │ │ ├── Admin.tsx │ │ └── Welcome.tsx │ ├── components │ │ ├── PageLoading │ │ │ └── index.tsx │ │ ├── HeaderDropdown │ │ │ ├── index.less │ │ │ └── index.tsx │ │ ├── SelectLang │ │ │ ├── index.less │ │ │ └── index.tsx │ │ ├── HeaderSearch │ │ │ ├── index.less │ │ │ └── index.tsx │ │ ├── NoticeIcon │ │ │ ├── index.less │ │ │ ├── NoticeList.less │ │ │ └── NoticeList.tsx │ │ ├── Footer │ │ │ └── index.tsx │ │ └── RightContent │ │ │ ├── index.less │ │ │ ├── index.tsx │ │ │ └── AvatarDropdown.tsx │ ├── access.ts │ ├── utils │ │ ├── utils.less │ │ └── utils.ts │ ├── app.tsx │ ├── services │ │ ├── API.d.ts │ │ ├── login.ts │ │ └── user.ts │ ├── global.less │ ├── typings.d.ts │ └── layouts │ │ └── BasicLayout.tsx ├── jsconfig.json ├── .eslintrc.js ├── jest.config.js ├── .editorconfig ├── .prettierignore ├── config │ ├── defaultSettings.ts │ ├── proxy.ts │ └── config.ts ├── .gitignore ├── tsconfig.json └── tests │ ├── PuppeteerEnvironment.js │ ├── getBrowser.js │ ├── beforeTest.js │ └── run-tests.js ├── .eslintignore ├── .stylelintrc.js ├── src ├── locales │ ├── en-US.ts │ ├── it-IT.ts │ ├── zh-CN.ts │ ├── zh-TW.ts │ ├── index.ts │ ├── zh-TW │ │ └── settingDrawer.ts │ ├── zh-CN │ │ └── settingDrawer.ts │ ├── it-IT │ │ └── settingDrawer.ts │ └── en-US │ │ └── settingDrawer.ts ├── Header.less ├── GridContent │ ├── GridContent.less │ └── index.tsx ├── PageLoading │ └── index.tsx ├── SiderMenu │ ├── SiderMenuUtils.ts │ ├── Counter.ts │ └── index.tsx ├── utils │ ├── compatibleLayout.ts │ ├── pathTools.ts │ ├── hooks.ts │ └── getMenuData.ts ├── FooterToolbar │ ├── index.less │ └── index.tsx ├── WrapContent.tsx ├── SettingDrawer │ ├── ThemeColor.less │ ├── RegionalChange.tsx │ ├── BlockCheckbox.tsx │ ├── index.less │ └── ThemeColor.tsx ├── GlobalFooter │ ├── index.less │ └── index.tsx ├── RouteContext.tsx ├── typings.ts ├── index.tsx ├── defaultSettings.ts ├── Footer.tsx ├── BasicLayout.less ├── TopNavHeader │ ├── index.less │ └── index.tsx ├── GlobalHeader │ └── index.less └── PageContainer │ └── index.less ├── jsconfig.json ├── .fatherrc.js ├── .prettierrc ├── .prettierignore ├── .github ├── ISSUE_TEMPLATE │ ├── feature_request.md │ ├── question.md │ └── bug_report.md └── workflows │ ├── rebase.yml │ ├── test.yml │ └── deploy.yml ├── jest.config.js ├── .editorconfig ├── .eslintrc.js ├── docs ├── demo │ ├── customMenu.ts │ ├── footer.tsx │ ├── hideMenu.tsx │ ├── materialMenu.tsx │ ├── customizeMenu.tsx │ ├── dynamicMenu.tsx │ ├── antd@3MenuIconFormServe.tsx │ ├── antd@4MenuIconFormServe.tsx │ ├── defaultProps.tsx │ ├── base.tsx │ └── api.tsx ├── footer.md ├── example.md ├── example │ ├── DefaultOpenAllMenu.tsx │ ├── TopmenuNested.tsx │ ├── Nested.tsx │ ├── IconFont.tsx │ ├── BreadcrumbsRepeat.tsx │ ├── searchMenu.tsx │ └── complexMenu.ts ├── index.md └── menu.md ├── tests ├── __tests__ │ ├── util.ts │ ├── defaultProps.ts │ ├── footer.test.tsx │ ├── settings.test.tsx │ ├── compatible.test.tsx │ ├── defaultSettings.ts │ ├── __snapshots__ │ │ └── footer.test.tsx.snap │ ├── pageHeaderWarp.test.tsx │ └── settingDrawer.test.tsx └── setupTests.js ├── .umirc.js ├── .gitignore ├── tsconfig-check.json ├── tsconfig.json └── LICENSE /public/CNAME: -------------------------------------------------------------------------------- 1 | prolayout.ant.design 2 | -------------------------------------------------------------------------------- /example/public/CNAME: -------------------------------------------------------------------------------- 1 | preview.pro.ant.design -------------------------------------------------------------------------------- /example/.eslintignore: -------------------------------------------------------------------------------- 1 | /lambda/ 2 | /scripts 3 | /config 4 | .history -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ant-design/ant-design-pro-layout/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /lambda/mock/** 2 | /scripts 3 | /config 4 | /example 5 | _test_ 6 | /node_modules 7 | /es 8 | /lib -------------------------------------------------------------------------------- /.stylelintrc.js: -------------------------------------------------------------------------------- 1 | const fabric = require('@umijs/fabric'); 2 | 3 | module.exports = { 4 | ...fabric.stylelint, 5 | }; 6 | -------------------------------------------------------------------------------- /example/public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ant-design/ant-design-pro-layout/HEAD/example/public/favicon.png -------------------------------------------------------------------------------- /example/public/home_bg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ant-design/ant-design-pro-layout/HEAD/example/public/home_bg.png -------------------------------------------------------------------------------- /example/.prettierrc.js: -------------------------------------------------------------------------------- 1 | const fabric = require('@umijs/fabric'); 2 | 3 | module.exports = { 4 | ...fabric.prettier, 5 | }; 6 | -------------------------------------------------------------------------------- /example/.stylelintrc.js: -------------------------------------------------------------------------------- 1 | const fabric = require('@umijs/fabric'); 2 | 3 | module.exports = { 4 | ...fabric.stylelint, 5 | }; 6 | -------------------------------------------------------------------------------- /src/locales/en-US.ts: -------------------------------------------------------------------------------- 1 | import settingDrawer from './en-US/settingDrawer'; 2 | 3 | export default { 4 | ...settingDrawer, 5 | }; 6 | -------------------------------------------------------------------------------- /src/locales/it-IT.ts: -------------------------------------------------------------------------------- 1 | import settingDrawer from './it-IT/settingDrawer'; 2 | 3 | export default { 4 | ...settingDrawer, 5 | }; 6 | -------------------------------------------------------------------------------- /src/locales/zh-CN.ts: -------------------------------------------------------------------------------- 1 | import settingDrawer from './zh-CN/settingDrawer'; 2 | 3 | export default { 4 | ...settingDrawer, 5 | }; 6 | -------------------------------------------------------------------------------- /src/locales/zh-TW.ts: -------------------------------------------------------------------------------- 1 | import settingDrawer from './zh-TW/settingDrawer'; 2 | 3 | export default { 4 | ...settingDrawer, 5 | }; 6 | -------------------------------------------------------------------------------- /example/public/icons/icon-128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ant-design/ant-design-pro-layout/HEAD/example/public/icons/icon-128x128.png -------------------------------------------------------------------------------- /example/public/icons/icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ant-design/ant-design-pro-layout/HEAD/example/public/icons/icon-192x192.png -------------------------------------------------------------------------------- /example/public/icons/icon-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ant-design/ant-design-pro-layout/HEAD/example/public/icons/icon-512x512.png -------------------------------------------------------------------------------- /example/mock/route.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | '/api/auth_routes': { 3 | '/form/advanced-form': { authority: ['admin', 'user'] }, 4 | }, 5 | }; 6 | -------------------------------------------------------------------------------- /example/src/locales/zh-CN/component.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'component.tagSelect.expand': '展开', 3 | 'component.tagSelect.collapse': '收起', 4 | 'component.tagSelect.all': '全部', 5 | }; 6 | -------------------------------------------------------------------------------- /example/src/locales/zh-TW/component.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'component.tagSelect.expand': '展開', 3 | 'component.tagSelect.collapse': '收起', 4 | 'component.tagSelect.all': '全部', 5 | }; 6 | -------------------------------------------------------------------------------- /example/src/locales/en-US/component.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'component.tagSelect.expand': 'Expand', 3 | 'component.tagSelect.collapse': 'Collapse', 4 | 'component.tagSelect.all': 'All', 5 | }; 6 | -------------------------------------------------------------------------------- /example/src/locales/pt-BR/component.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'component.tagSelect.expand': 'Expandir', 3 | 'component.tagSelect.collapse': 'Diminuir', 4 | 'component.tagSelect.all': 'Todas', 5 | }; 6 | -------------------------------------------------------------------------------- /example/src/pages/Welcome.less: -------------------------------------------------------------------------------- 1 | @import '~antd/es/style/themes/default.less'; 2 | 3 | .pre { 4 | margin: 12px 0; 5 | padding: 12px 20px; 6 | background: @input-bg; 7 | box-shadow: @card-shadow; 8 | } 9 | -------------------------------------------------------------------------------- /jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "emitDecoratorMetadata": true, 4 | "experimentalDecorators": true, 5 | "baseUrl": ".", 6 | "paths": { 7 | "@/*": ["./src/*"] 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /example/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "emitDecoratorMetadata": true, 4 | "experimentalDecorators": true, 5 | "baseUrl": ".", 6 | "paths": { 7 | "@/*": ["./src/*"] 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /example/src/components/PageLoading/index.tsx: -------------------------------------------------------------------------------- 1 | import { PageLoading } from '../../../../src'; 2 | 3 | // loading components from code split 4 | // https://umijs.org/plugin/umi-plugin-react.html#dynamicimport 5 | export default PageLoading; 6 | -------------------------------------------------------------------------------- /src/Header.less: -------------------------------------------------------------------------------- 1 | @import '~antd/es/style/themes/default.less'; 2 | 3 | @pro-layout-fixed-header-prefix-cls: ~'@{ant-prefix}-pro-fixed-header'; 4 | 5 | .@{pro-layout-fixed-header-prefix-cls} { 6 | z-index: 9; 7 | width: 100%; 8 | } 9 | -------------------------------------------------------------------------------- /.fatherrc.js: -------------------------------------------------------------------------------- 1 | export default { 2 | entry: 'src/index.tsx', 3 | esm: { 4 | type: 'babel', 5 | importLibToEs: true, 6 | }, 7 | cjs: 'babel', 8 | extraBabelPlugins: [['import', { libraryName: 'antd', style: true }]], 9 | }; 10 | -------------------------------------------------------------------------------- /example/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [require.resolve('@umijs/fabric/dist/eslint')], 3 | globals: { 4 | ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION: true, 5 | page: true, 6 | REACT_APP_ENV: true, 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /example/src/locales/zh-CN/pwa.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'app.pwa.offline': '当前处于离线状态', 3 | 'app.pwa.serviceworker.updated': '有新内容', 4 | 'app.pwa.serviceworker.updated.hint': '请点击“刷新”按钮或者手动刷新页面', 5 | 'app.pwa.serviceworker.updated.ok': '刷新', 6 | }; 7 | -------------------------------------------------------------------------------- /example/src/locales/zh-TW/pwa.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'app.pwa.offline': '當前處於離線狀態', 3 | 'app.pwa.serviceworker.updated': '有新內容', 4 | 'app.pwa.serviceworker.updated.hint': '請點擊“刷新”按鈕或者手動刷新頁面', 5 | 'app.pwa.serviceworker.updated.ok': '刷新', 6 | }; 7 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all", 4 | "proseWrap": "never", 5 | "overrides": [ 6 | { 7 | "files": ".prettierrc", 8 | "options": { 9 | "parser": "json" 10 | } 11 | } 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /example/src/access.ts: -------------------------------------------------------------------------------- 1 | // src/access.ts 2 | export default function (initialState: { currentUser?: API.CurrentUser | undefined }) { 3 | const { currentUser } = initialState || {}; 4 | return { 5 | canAdmin: currentUser && currentUser.access === 'admin', 6 | }; 7 | } 8 | -------------------------------------------------------------------------------- /example/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testURL: 'http://localhost:8000', 3 | testEnvironment: './tests/PuppeteerEnvironment', 4 | verbose: false, 5 | globals: { 6 | ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION: false, 7 | localStorage: null, 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | **/*.svg 2 | package.json 3 | .umi 4 | .umi-production 5 | /dist 6 | .dockerignore 7 | .DS_Store 8 | .eslintignore 9 | *.png 10 | *.toml 11 | docker 12 | .editorconfig 13 | Dockerfile* 14 | .gitignore 15 | .prettierignore 16 | LICENSE 17 | .eslintcache 18 | *.lock 19 | yarn-error.log -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: '功能需求 ✨' 3 | about: 对 Ant Design Pro Layout 的需求或建议 4 | title: '👑 [需求]' 5 | labels: '👑 Feature' 6 | assignees: '' 7 | --- 8 | 9 | ### 🥰 需求描述 [详细地描述需求,让大家都能理解] 10 | 11 | ### 🧐 解决方案 [如果你有解决方案,在这里清晰地阐述] 12 | 13 | ### 🚑 其他信息 [如截图等其他信息可以贴在这里] 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: '疑问或需要帮助 ❓' 3 | about: 对 Ant Design Pro Layout 使用的疑问或需要帮助 4 | title: '🧐[问题]' 5 | labels: '🧐 Question' 6 | assignees: '' 7 | --- 8 | 9 | ### 🧐 问题描述 [详细地描述问题,让大家都能理解] 10 | 11 | ### 💻 示例代码 [如果有必要,展示代码,线上示例,或仓库] 12 | 13 | ### 🚑 其他信息 [如截图等其他信息可以贴在这里] 14 | -------------------------------------------------------------------------------- /src/GridContent/GridContent.less: -------------------------------------------------------------------------------- 1 | @import '~antd/es/style/themes/default.less'; 2 | @pro-layout-grid-content-prefix-cls: ~'@{ant-prefix}-pro-grid-content'; 3 | 4 | .@{pro-layout-grid-content-prefix-cls} { 5 | width: 100%; 6 | &.wide { 7 | max-width: 1200px; 8 | margin: 0 auto; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testURL: 'http://localhost', 3 | verbose: true, 4 | snapshotSerializers: [require.resolve('enzyme-to-json/serializer')], 5 | extraSetupFiles: ['./tests/setupTests.js'], 6 | globals: { 7 | ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION: false, 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /example/src/locales/en-US/pwa.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'app.pwa.offline': 'You are offline now', 3 | 'app.pwa.serviceworker.updated': 'New content is available', 4 | 'app.pwa.serviceworker.updated.hint': 'Please press the "Refresh" button to reload current page', 5 | 'app.pwa.serviceworker.updated.ok': 'Refresh', 6 | }; 7 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | 15 | [Makefile] 16 | indent_style = tab 17 | -------------------------------------------------------------------------------- /src/PageLoading/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Spin } from 'antd'; 3 | 4 | const PageLoading: React.FC<{ 5 | tip?: string; 6 | }> = ({ tip }) => ( 7 |
8 | 9 |
10 | ); 11 | 12 | export default PageLoading; 13 | -------------------------------------------------------------------------------- /example/.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | 15 | [Makefile] 16 | indent_style = tab 17 | -------------------------------------------------------------------------------- /example/src/utils/utils.less: -------------------------------------------------------------------------------- 1 | // mixins for clearfix 2 | // ------------------------ 3 | .clearfix() { 4 | zoom: 1; 5 | &::before, 6 | &::after { 7 | display: table; 8 | content: ' '; 9 | } 10 | &::after { 11 | clear: both; 12 | height: 0; 13 | font-size: 0; 14 | visibility: hidden; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /example/.prettierignore: -------------------------------------------------------------------------------- 1 | **/*.svg 2 | package.json 3 | .umi 4 | .umi-production 5 | /dist 6 | .dockerignore 7 | .DS_Store 8 | .eslintignore 9 | *.png 10 | *.toml 11 | docker 12 | .editorconfig 13 | Dockerfile* 14 | .gitignore 15 | .prettierignore 16 | LICENSE 17 | .eslintcache 18 | *.lock 19 | yarn-error.log 20 | .history 21 | CNAME 22 | /build 23 | -------------------------------------------------------------------------------- /example/src/locales/pt-BR/pwa.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'app.pwa.offline': 'Você está offline agora', 3 | 'app.pwa.serviceworker.updated': 'Novo conteúdo está disponível', 4 | 'app.pwa.serviceworker.updated.hint': 5 | 'Por favor, pressione o botão "Atualizar" para recarregar a página atual', 6 | 'app.pwa.serviceworker.updated.ok': 'Atualizar', 7 | }; 8 | -------------------------------------------------------------------------------- /src/SiderMenu/SiderMenuUtils.ts: -------------------------------------------------------------------------------- 1 | import { getMatchMenu, MenuDataItem } from '@umijs/route-utils'; 2 | 3 | // 获取当前的选中菜单 4 | export const getSelectedMenuKeys = ( 5 | pathname: string, 6 | menuData: MenuDataItem[], 7 | ): string[] => { 8 | const menus = getMatchMenu(pathname, menuData); 9 | return menus.map((item) => item.key || item.path || ''); 10 | }; 11 | -------------------------------------------------------------------------------- /src/SiderMenu/Counter.ts: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import { createContainer } from 'unstated-next'; 3 | 4 | function useMenuCounter() { 5 | const [flatMenuKeys, setFlatMenuKeys] = useState([]); 6 | return { 7 | flatMenuKeys, 8 | setFlatMenuKeys, 9 | }; 10 | } 11 | 12 | const MenuCounter = createContainer(useMenuCounter); 13 | export default MenuCounter; 14 | -------------------------------------------------------------------------------- /.github/workflows/rebase.yml: -------------------------------------------------------------------------------- 1 | on: 2 | issue_comment: 3 | types: [created] 4 | name: Automatic Rebase 5 | jobs: 6 | rebase: 7 | name: Rebase 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@master 11 | - name: Automatic Rebase 12 | uses: cirrus-actions/rebase@master 13 | env: 14 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 15 | -------------------------------------------------------------------------------- /src/utils/compatibleLayout.ts: -------------------------------------------------------------------------------- 1 | const compatibleLayout = ( 2 | layout?: 'side' | 'top' | 'mix' | 'sidemenu' | 'topmenu', 3 | ) => { 4 | if (!layout) { 5 | return layout; 6 | } 7 | const layoutEnum = ['sidemenu', 'topmenu']; 8 | if (layoutEnum.includes(layout)) { 9 | return layout.replace('menu', ''); 10 | } 11 | return layout; 12 | }; 13 | 14 | export default compatibleLayout; 15 | -------------------------------------------------------------------------------- /example/src/components/HeaderDropdown/index.less: -------------------------------------------------------------------------------- 1 | @import '~antd/es/style/themes/default.less'; 2 | 3 | .container > * { 4 | background-color: @popover-bg; 5 | border-radius: 4px; 6 | box-shadow: @shadow-1-down; 7 | } 8 | 9 | @media screen and (max-width: @screen-xs) { 10 | .container { 11 | width: 100% !important; 12 | } 13 | .container > * { 14 | border-radius: 0 !important; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [require.resolve('@umijs/fabric/dist/eslint')], 3 | globals: { 4 | ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION: true, 5 | page: true, 6 | }, 7 | rules: { 8 | 'import/no-extraneous-dependencies': 0, 9 | 'import/no-named-as-default-member': 0, 10 | 'import/default': 0, 11 | 'import/no-named-as-default-member': 0, 12 | 'import/named': 0, 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /src/utils/pathTools.ts: -------------------------------------------------------------------------------- 1 | // /userInfo/2144/id => ['/userInfo','/userInfo/2144,'/userInfo/2144/id'] 2 | // eslint-disable-next-line import/prefer-default-export 3 | export function urlToList(url?: string): string[] { 4 | if (!url || url === '/') { 5 | return ['/']; 6 | } 7 | const urlList = url.split('/').filter((i) => i); 8 | return urlList.map( 9 | (urlItem, index) => `/${urlList.slice(0, index + 1).join('/')}`, 10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /example/src/app.tsx: -------------------------------------------------------------------------------- 1 | import { queryCurrent } from './services/user'; 2 | import defaultSettings from '../config/defaultSettings'; 3 | import { Settings } from '../../src/'; 4 | 5 | export async function getInitialState(): Promise<{ 6 | currentUser?: API.CurrentUser; 7 | settings?: Partial; 8 | }> { 9 | const currentUser = await queryCurrent(); 10 | return { 11 | currentUser, 12 | settings: defaultSettings, 13 | }; 14 | } 15 | -------------------------------------------------------------------------------- /example/src/pages/user/login/components/Login/LoginContext.tsx: -------------------------------------------------------------------------------- 1 | import { createContext } from 'react'; 2 | 3 | export interface LoginContextProps { 4 | tabUtil?: { 5 | addTab: (id: string) => void; 6 | removeTab: (id: string) => void; 7 | }; 8 | updateActive?: (activeItem: { [key: string]: string } | string) => void; 9 | } 10 | 11 | const LoginContext: React.Context = createContext({}); 12 | 13 | export default LoginContext; 14 | -------------------------------------------------------------------------------- /docs/demo/customMenu.ts: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | path: '/', 4 | name: 'welcome', 5 | children: [ 6 | { 7 | path: '/welcome', 8 | name: 'one', 9 | children: [ 10 | { 11 | path: '/welcome/welcome', 12 | name: 'two', 13 | exact: true, 14 | }, 15 | ], 16 | }, 17 | ], 18 | }, 19 | { 20 | path: '/demo', 21 | name: 'demo', 22 | }, 23 | ]; 24 | -------------------------------------------------------------------------------- /tests/__tests__/util.ts: -------------------------------------------------------------------------------- 1 | import { act } from 'react-dom/test-utils'; 2 | 3 | export const waitForComponentToPaint = async (wrapper: any) => { 4 | await act(async () => { 5 | await new Promise((resolve) => setTimeout(resolve, 100)); 6 | wrapper.update(); 7 | }); 8 | }; 9 | 10 | export const waitTime = (time: number) => { 11 | return new Promise((resolve) => { 12 | setTimeout(() => { 13 | resolve(true); 14 | }, time); 15 | }); 16 | }; 17 | -------------------------------------------------------------------------------- /example/config/defaultSettings.ts: -------------------------------------------------------------------------------- 1 | import { Settings } from '../../src/'; 2 | 3 | export default { 4 | navTheme: 'light', 5 | // 拂晓蓝 6 | primaryColor: '#1890ff', 7 | layout: 'mix', 8 | contentWidth: 'Fluid', 9 | fixedHeader: false, 10 | autoHideHeader: false, 11 | fixSiderbar: true, 12 | colorWeak: false, 13 | menu: { 14 | locale: true, 15 | }, 16 | title: 'Ant Design Pro', 17 | pwa: false, 18 | iconfontUrl: '', 19 | headerHeight: 64, 20 | } as Settings; 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: '报告Bug 🐛' 3 | about: 报告 Ant Design Pro Layout 的 bug 4 | title: '🐛[BUG]' 5 | labels: '🐛 BUG' 6 | assignees: '' 7 | --- 8 | 9 | ### 🐛 bug 描述 [详细地描述 bug,让大家都能理解] 10 | 11 | ### 📷 复现步骤 [清晰描述复现步骤,让别人也能看到问题] 12 | 13 | ### 🏞 期望结果 [描述你原本期望看到的结果] 14 | 15 | ### 💻 复现代码 [提供可复现的代码,仓库,或线上示例] 16 | 17 | ### © 版本信息 18 | 19 | - Ant Design Pro 版本: [e.g. 4.0.0] 20 | - umi 版本 21 | - 浏览器环境 22 | - 开发环境 [e.g. mac OS] 23 | 24 | ### 🚑 其他信息 [如截图等其他信息可以贴在这里] 25 | -------------------------------------------------------------------------------- /example/src/pages/404.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Result } from 'antd'; 2 | import React from 'react'; 3 | import { history } from 'umi'; 4 | 5 | const NoFoundPage: React.FC<{}> = () => ( 6 | history.push('/')}> 12 | Back Home 13 | 14 | } 15 | /> 16 | ); 17 | 18 | export default NoFoundPage; 19 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Test CI 2 | on: [push, pull_request] 3 | jobs: 4 | test: 5 | runs-on: ubuntu-latest 6 | 7 | steps: 8 | - name: checkout 9 | uses: actions/checkout@master 10 | 11 | - name: install 12 | run: npm install 13 | 14 | - name: lint 15 | run: npm run lint && npm run tsc 16 | - name: test 17 | run: npm run test:coverage 18 | - name: Generate coverage 19 | run: bash <(curl -s https://codecov.io/bash) 20 | -------------------------------------------------------------------------------- /example/src/services/API.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace API { 2 | export interface CurrentUser { 3 | avatar?: string; 4 | name?: string; 5 | title?: string; 6 | group?: string; 7 | signature?: string; 8 | tags?: { 9 | key: string; 10 | label: string; 11 | }[]; 12 | userid?: string; 13 | access?: 'user' | 'guest' | 'admin'; 14 | unreadCount?: number; 15 | } 16 | 17 | export interface LoginStateType { 18 | status?: 'ok' | 'error'; 19 | type?: string; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/FooterToolbar/index.less: -------------------------------------------------------------------------------- 1 | @import '~antd/es/style/themes/default.less'; 2 | 3 | @pro-footer-bar-prefix-cls: ~'@{ant-prefix}-pro-footer-bar'; 4 | 5 | .@{pro-footer-bar-prefix-cls} { 6 | position: fixed; 7 | right: 0; 8 | bottom: 0; 9 | z-index: 99; 10 | display: flex; 11 | width: 100%; 12 | padding: 0 24px; 13 | line-height: 48px; 14 | background: @component-background; 15 | border-top: 1px solid @border-color-split; 16 | box-shadow: @box-shadow-base; 17 | &-left { 18 | flex: 1; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/WrapContent.tsx: -------------------------------------------------------------------------------- 1 | import React, { CSSProperties } from 'react'; 2 | import { Layout } from 'antd'; 3 | 4 | const WrapContent: React.FC<{ 5 | isChildrenLayout?: boolean; 6 | className?: string; 7 | style?: CSSProperties; 8 | location?: any; 9 | contentHeight?: number | string; 10 | }> = (props) => { 11 | const { style, className, children } = props; 12 | return ( 13 | 14 | {children} 15 | 16 | ); 17 | }; 18 | 19 | export default WrapContent; 20 | -------------------------------------------------------------------------------- /example/src/components/SelectLang/index.less: -------------------------------------------------------------------------------- 1 | @import '~antd/es/style/themes/default.less'; 2 | 3 | .menu { 4 | :global(.anticon) { 5 | margin-right: 8px; 6 | } 7 | :global(.ant-dropdown-menu-item) { 8 | min-width: 160px; 9 | } 10 | } 11 | 12 | .dropDown { 13 | line-height: @layout-header-height; 14 | vertical-align: top; 15 | cursor: pointer; 16 | > span { 17 | font-size: 16px !important; 18 | transform: none !important; 19 | svg { 20 | position: relative; 21 | top: -1px; 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /example/src/components/HeaderSearch/index.less: -------------------------------------------------------------------------------- 1 | @import '~antd/es/style/themes/default.less'; 2 | 3 | .headerSearch { 4 | .input { 5 | width: 0; 6 | min-width: 0; 7 | overflow: hidden; 8 | background: transparent; 9 | border-radius: 0; 10 | transition: width 0.3s, margin-left 0.3s; 11 | :global(.ant-select-selection) { 12 | background: transparent; 13 | } 14 | input { 15 | box-shadow: none !important; 16 | } 17 | 18 | &.show { 19 | width: 210px; 20 | margin-left: 8px; 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /example/src/components/NoticeIcon/index.less: -------------------------------------------------------------------------------- 1 | @import '~antd/es/style/themes/default.less'; 2 | 3 | .popover { 4 | position: relative; 5 | width: 336px; 6 | } 7 | 8 | .noticeButton { 9 | display: inline-block; 10 | cursor: pointer; 11 | transition: all 0.3s; 12 | } 13 | .icon { 14 | padding: 4px; 15 | vertical-align: middle; 16 | } 17 | 18 | .badge { 19 | font-size: 16px; 20 | } 21 | 22 | .tabs { 23 | :global { 24 | .ant-tabs-nav-scroll { 25 | text-align: center; 26 | } 27 | .ant-tabs-bar { 28 | margin-bottom: 0; 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/SettingDrawer/ThemeColor.less: -------------------------------------------------------------------------------- 1 | @import './index.less'; 2 | .@{ant-pro-setting-drawer}-content { 3 | .theme-color { 4 | margin-top: 24px; 5 | overflow: hidden; 6 | .theme-color-title { 7 | margin-bottom: 12px; 8 | font-size: 14px; 9 | line-height: 22px; 10 | } 11 | .theme-color-block { 12 | float: left; 13 | width: 20px; 14 | height: 20px; 15 | margin-right: 8px; 16 | color: #fff; 17 | font-weight: bold; 18 | text-align: center; 19 | border-radius: 2px; 20 | cursor: pointer; 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /tests/__tests__/defaultProps.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | route: { 3 | path: '/', 4 | routes: [ 5 | { 6 | path: '/', 7 | name: 'welcome', 8 | routes: [ 9 | { 10 | path: '/welcome', 11 | name: 'one', 12 | routes: [ 13 | { 14 | path: '/welcome/welcome', 15 | name: 'two', 16 | exact: true, 17 | }, 18 | ], 19 | }, 20 | ], 21 | }, 22 | ], 23 | }, 24 | location: { 25 | pathname: '/', 26 | }, 27 | }; 28 | -------------------------------------------------------------------------------- /src/utils/hooks.ts: -------------------------------------------------------------------------------- 1 | import { useEffect } from 'react'; 2 | import { isBrowser } from './utils'; 3 | import defaultSettings from '../defaultSettings'; 4 | 5 | export function useDocumentTitle( 6 | titleInfo: { 7 | title: string; 8 | id: string; 9 | pageName: string; 10 | }, 11 | appDefaultTitle: string = defaultSettings.title, 12 | ) { 13 | const titleText = 14 | typeof titleInfo.pageName === 'string' ? titleInfo.title : appDefaultTitle; 15 | useEffect(() => { 16 | if (isBrowser() && titleText) { 17 | document.title = titleText; 18 | } 19 | }, [titleInfo.title]); 20 | } 21 | -------------------------------------------------------------------------------- /.umirc.js: -------------------------------------------------------------------------------- 1 | export default { 2 | title: 'ProLayout', 3 | logo: 'https://gw.alipayobjects.com/zos/rmsportal/KDpgvguMpGfqaHPjicRK.svg', 4 | mode: 'site', 5 | extraBabelPlugins: [ 6 | [ 7 | 'import', 8 | { 9 | libraryName: 'antd', 10 | libraryDirectory: 'es', 11 | style: 'css', 12 | }, 13 | ], 14 | ], 15 | navs: [ 16 | null, 17 | { 18 | title: 'GitHub', 19 | path: 'https://github.com/ant-design/ant-design-pro-layout', 20 | }, 21 | ], 22 | dynamicImport: { 23 | loading: '@ant-design/pro-skeleton', 24 | }, 25 | hash: true, 26 | }; 27 | -------------------------------------------------------------------------------- /example/src/pages/ListTableList/components/CreateForm.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Modal } from 'antd'; 3 | 4 | interface CreateFormProps { 5 | modalVisible: boolean; 6 | onCancel: () => void; 7 | } 8 | 9 | const CreateForm: React.FC = (props) => { 10 | const { modalVisible, onCancel } = props; 11 | 12 | return ( 13 | onCancel()} 18 | footer={null} 19 | > 20 | {props.children} 21 | 22 | ); 23 | }; 24 | 25 | export default CreateForm; 26 | -------------------------------------------------------------------------------- /example/src/locales/zh-TW.ts: -------------------------------------------------------------------------------- 1 | import component from './zh-TW/component'; 2 | import globalHeader from './zh-TW/globalHeader'; 3 | import menu from './zh-TW/menu'; 4 | import pwa from './zh-TW/pwa'; 5 | import settingDrawer from './zh-TW/settingDrawer'; 6 | import settings from './zh-TW/settings'; 7 | 8 | export default { 9 | 'navBar.lang': '語言', 10 | 'layout.user.link.help': '幫助', 11 | 'layout.user.link.privacy': '隱私', 12 | 'layout.user.link.terms': '條款', 13 | 'app.preview.down.block': '下載此頁面到本地項目', 14 | ...globalHeader, 15 | ...menu, 16 | ...settingDrawer, 17 | ...settings, 18 | ...pwa, 19 | ...component, 20 | }; 21 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | **/node_modules 5 | # roadhog-api-doc ignore 6 | /src/utils/request-temp.js 7 | _roadhog-api-doc 8 | 9 | # production 10 | /dist 11 | /.vscode 12 | 13 | # misc 14 | .DS_Store 15 | npm-debug.log* 16 | yarn-error.log 17 | 18 | /coverage 19 | .idea 20 | yarn.lock 21 | package-lock.json 22 | *bak 23 | .vscode 24 | 25 | # visual studio code 26 | .history 27 | *.log 28 | functions/* 29 | .temp/** 30 | 31 | # umi 32 | .umi 33 | .umi-production 34 | 35 | # screenshot 36 | screenshot 37 | .firebase 38 | .eslintcache 39 | 40 | build 41 | -------------------------------------------------------------------------------- /example/src/services/login.ts: -------------------------------------------------------------------------------- 1 | import { request } from 'umi'; 2 | 3 | export interface LoginParamsType { 4 | userName: string; 5 | password: string; 6 | mobile: string; 7 | captcha: string; 8 | type: string; 9 | } 10 | 11 | export async function fakeAccountLogin(params: LoginParamsType) { 12 | return request('/api/login/account', { 13 | method: 'POST', 14 | data: params, 15 | }); 16 | } 17 | 18 | export async function getFakeCaptcha(mobile: string) { 19 | return request(`/api/login/captcha?mobile=${mobile}`); 20 | } 21 | 22 | export async function outLogin() { 23 | return request('/api/login/outLogin'); 24 | } 25 | -------------------------------------------------------------------------------- /docs/footer.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: 页脚 3 | order: 7 4 | side: false 5 | nav: 6 | title: 页脚 7 | order: 7 8 | --- 9 | 10 | # footer 的各种操作 11 | 12 | ProLayout 默认不提供页脚,要是和 Pro 官网相同的样式,需要自己引入一下页脚。 13 | 14 | ## 自定义页脚 15 | 16 | 17 | 18 | ## 相关 API 展示 19 | 20 | ### ProLayout 21 | 22 | | 参数 | 说明 | 类型 | 默认值 | 23 | | --- | --- | --- | --- | 24 | | footerRender | 自定义页脚的 render 方法 | (props: BasicLayoutProps) => ReactNode | - | 25 | 26 | ### DefaultFooter 27 | 28 | | 参数 | 说明 | 类型 | 默认值 | 29 | | --- | --- | --- | --- | 30 | | links | 默认自带的一些 | false \| `{key:string,title:string,href:string}[]` | - | 31 | | copyright | 版权声明文字 | ReactNode | - | 32 | -------------------------------------------------------------------------------- /src/GlobalFooter/index.less: -------------------------------------------------------------------------------- 1 | @import '~antd/es/style/themes/default.less'; 2 | 3 | @pro-global-footer-prefix-cls: ~'@{ant-prefix}-pro-global-footer'; 4 | 5 | .@{pro-global-footer-prefix-cls} { 6 | margin: 48px 0 24px 0; 7 | padding: 0 16px; 8 | text-align: center; 9 | 10 | &-links { 11 | margin-bottom: 8px; 12 | 13 | a { 14 | color: @text-color-secondary; 15 | transition: all 0.3s; 16 | 17 | &:not(:last-child) { 18 | margin-right: 40px; 19 | } 20 | 21 | &:hover { 22 | color: @text-color; 23 | } 24 | } 25 | } 26 | 27 | &-copyright { 28 | color: @text-color-secondary; 29 | font-size: @font-size-base; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | **/node_modules 5 | # roadhog-api-doc ignore 6 | /src/utils/request-temp.js 7 | _roadhog-api-doc 8 | 9 | # production 10 | **/dist 11 | /.vscode 12 | /es 13 | /lib 14 | 15 | # misc 16 | .DS_Store 17 | storybook-static 18 | npm-debug.log* 19 | yarn-error.log 20 | 21 | /coverage 22 | .idea 23 | yarn.lock 24 | package-lock.json 25 | *bak 26 | .vscode 27 | 28 | # visual studio code 29 | .history 30 | *.log 31 | functions/* 32 | lambda/mock/index.js 33 | .temp/** 34 | 35 | # umi 36 | .umi 37 | .umi-production 38 | 39 | # screenshot 40 | screenshot 41 | .firebase 42 | example/.temp/* 43 | .eslintcache 44 | -------------------------------------------------------------------------------- /docs/example.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Example 展示 3 | order: 1 4 | side: false 5 | nav: 6 | title: 示例 7 | order: 10 8 | --- 9 | 10 | # Example 展示 11 | 12 | 这里用于展示 ProLayout 的各种应用,如果你觉得你的用法能帮助到别人,欢迎 PR。 13 | 14 | ## 搜索菜单 15 | 16 | 17 | 18 | ## 多个路由对应一个菜单项 19 | 20 | 21 | 22 | ## 默认打开所有菜单 23 | 24 | 25 | 26 | ## 带参数的面包屑 27 | 28 | 29 | 30 | ## IconFont 31 | 32 | 33 | 34 | ## 嵌套布局 35 | 36 | 37 | 38 | ## 另外一种嵌套布局 39 | 40 | 41 | -------------------------------------------------------------------------------- /docs/example/DefaultOpenAllMenu.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 complexMenu from './complexMenu'; 5 | 6 | export default () => ( 7 |
13 | 22 | 23 |
Hello World
24 |
25 |
26 |
27 | ); 28 | -------------------------------------------------------------------------------- /tsconfig-check.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "build/dist", 4 | "module": "esnext", 5 | "target": "ES2015", 6 | "lib": ["esnext", "dom"], 7 | "sourceMap": true, 8 | "baseUrl": ".", 9 | "jsx": "react", 10 | "allowSyntheticDefaultImports": true, 11 | "moduleResolution": "node", 12 | "forceConsistentCasingInFileNames": true, 13 | "noImplicitReturns": true, 14 | "suppressImplicitAnyIndexErrors": true, 15 | "noUnusedLocals": true, 16 | "experimentalDecorators": true, 17 | "strict": true, 18 | "declaration": true, 19 | "skipLibCheck": true, 20 | "paths": { 21 | "@ant-design/pro-layout": ["./src"] 22 | } 23 | }, 24 | "include": ["src/"] 25 | } 26 | -------------------------------------------------------------------------------- /example/src/pages/user/login/components/Login/LoginSubmit.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Form } from 'antd'; 2 | 3 | import { ButtonProps } from 'antd/es/button'; 4 | import React from 'react'; 5 | import classNames from 'classnames'; 6 | import styles from './index.less'; 7 | 8 | const FormItem = Form.Item; 9 | 10 | interface LoginSubmitProps extends ButtonProps { 11 | className?: string; 12 | } 13 | 14 | const LoginSubmit: React.FC = ({ className, ...rest }) => { 15 | const clsString = classNames(styles.submit, className); 16 | return ( 17 | 18 | 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 |
{ 36 | window.open('https://remaxjs.org/'); 37 | }} 38 | > 39 | {logo} 40 | {title} 41 |
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 | {item.key} 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 |
41 | {links && ( 42 |
43 | {links.map((link) => ( 44 | 50 | {link.title} 51 | 52 | ))} 53 |
54 | )} 55 | {copyright && ( 56 |
{copyright}
57 | )} 58 |
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 | 35 | {locales.map((locale) => ( 36 | 37 | 38 | {languageIcons[locale]} 39 | {' '} 40 | {languageLabels[locale]} 41 | 42 | ))} 43 | 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 |
54 | 59 | Copyright {copyright || defaultCopyright} 60 | 61 | ) 62 | } 63 | /> 64 |
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={() =>