diff --git a/.gitignore b/.gitignore index fcac945fba1338aa06a49ec7b976af0cd3f08977..bdba67fc8fb7b7f8e321508ec03ef324f7079070 100755 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,4 @@ yarn-error.log .idea yarn.lock package-lock.json +jsconfig.json diff --git a/package.json b/package.json index e227c5bd32c08dba96fc834b931504b35620164e..8db9eb0b8f2206f0e3f6678430afb670bcecbfc6 100755 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "moment": "^2.19.1", "numeral": "^2.0.6", "omit.js": "^1.0.0", + "path-to-regexp": "^2.1.0", "prop-types": "^15.5.10", "qs": "^6.5.0", "rc-drawer-menu": "^0.5.0", diff --git a/src/common/router.js b/src/common/router.js index 0b45dcd4cc1baa9c67ea9eeb1191479b584ee891..5df24776509e6f111ce44204585a9bb04d878530 100644 --- a/src/common/router.js +++ b/src/common/router.js @@ -1,5 +1,6 @@ import { createElement } from 'react'; import dynamic from 'dva/dynamic'; +import pathToRegexp from 'path-to-regexp'; import { getMenuData } from './menu'; let routerDataCache; @@ -165,14 +166,31 @@ export const getRouterData = (app) => { }; // Get name from ./menu.js or just set it in the router data. const menuData = getFlatMenuData(getMenuData()); + + // Route configuration data + // eg. {name,authority ...routerConfig } const routerData = {}; - Object.keys(routerConfig).forEach((item) => { - const menuItem = menuData[item.replace(/^\//, '')] || {}; - routerData[item] = { - ...routerConfig[item], - name: routerConfig[item].name || menuItem.name, - authority: routerConfig[item].authority || menuItem.authority, + // The route matches the menu + Object.keys(routerConfig).forEach((path) => { + // Regular match item name + // eg. router /user/:id === /user/chen + const pathRegexp = pathToRegexp(path); + const menuKey = Object.keys(menuData).find(key => pathRegexp.test(`/${key}`)); + let menuItem = {}; + // If menuKey is not empty + if (menuKey) { + menuItem = menuData[menuKey]; + } + let router = routerConfig[path]; + // If you need to configure complex parameter routing, + // https://github.com/ant-design/ant-design-pro-site/blob/master/docs/router-and-nav.md#%E5%B8%A6%E5%8F%82%E6%95%B0%E7%9A%84%E8%B7%AF%E7%94%B1%E8%8F%9C%E5%8D%95 + // eg . /list/:type/user/info/:id + router = { + ...router, + name: router.name || menuItem.name, + authority: router.authority || menuItem.authority, }; + routerData[path] = router; }); return routerData; }; diff --git a/src/components/PageHeader/index.js b/src/components/PageHeader/index.js index 57156fc1c0fb9a5ba04cb7ef397b63f587e5a486..4be9af62f4ee7f11e3c35315935125b281dd0897 100644 --- a/src/components/PageHeader/index.js +++ b/src/components/PageHeader/index.js @@ -1,24 +1,17 @@ import React, { PureComponent, createElement } from 'react'; import PropTypes from 'prop-types'; +import pathToRegexp from 'path-to-regexp'; import { Breadcrumb, Tabs } from 'antd'; import classNames from 'classnames'; import styles from './index.less'; + const { TabPane } = Tabs; function getBreadcrumb(breadcrumbNameMap, url) { - if (breadcrumbNameMap[url]) { - return breadcrumbNameMap[url]; - } - const urlWithoutSplash = url.replace(/\/$/, ''); - if (breadcrumbNameMap[urlWithoutSplash]) { - return breadcrumbNameMap[urlWithoutSplash]; - } let breadcrumb = {}; Object.keys(breadcrumbNameMap).forEach((item) => { - const itemRegExpStr = `^${item.replace(/:[\w-]+/g, '[\\w-]+')}$`; - const itemRegExp = new RegExp(itemRegExpStr); - if (itemRegExp.test(url)) { + if (pathToRegexp(item).test(url)) { breadcrumb = breadcrumbNameMap[item]; } }); @@ -41,10 +34,90 @@ export default class PageHeader extends PureComponent { return { routes: this.props.routes || this.context.routes, params: this.props.params || this.context.params, - location: this.props.location || this.context.location, + routerLocation: this.props.location || this.context.location, breadcrumbNameMap: this.props.breadcrumbNameMap || this.context.breadcrumbNameMap, }; }; + // Generated according to props + conversionFromProps= () => { + const { + breadcrumbList, linkElement = 'a', + } = this.props; + return ( + + {breadcrumbList.map(item => ( + + {item.href ? (createElement(linkElement, { + [linkElement === 'a' ? 'href' : 'to']: item.href, + }, item.title)) : item.title} + + ))} + + ); + } + conversionFromLocation = (routerLocation, breadcrumbNameMap) => { + const { linkElement = 'a' } = this.props; + // Convert the path to an array + const pathSnippets = routerLocation.pathname.split('/').filter(i => i); + // Loop data mosaic routing + const extraBreadcrumbItems = pathSnippets.map((_, index) => { + const url = `/${pathSnippets.slice(0, index + 1).join('/')}`; + const currentBreadcrumb = getBreadcrumb(breadcrumbNameMap, url); + const isLinkable = (index !== pathSnippets.length - 1) && currentBreadcrumb.component; + return currentBreadcrumb.name && !currentBreadcrumb.hideInBreadcrumb ? ( + + {createElement( + isLinkable ? linkElement : 'span', + { [linkElement === 'a' ? 'href' : 'to']: url }, + currentBreadcrumb.name, + )} + + ) : null; + }); + // Add home breadcrumbs to your head + extraBreadcrumbItems.unshift( + + {createElement(linkElement, { + [linkElement === 'a' ? 'href' : 'to']: '/' }, '首页')} + + ); + return ( + + {extraBreadcrumbItems} + + ); + } + /** + * 将参数转化为面包屑 + * Convert parameters into breadcrumbs + */ + conversionBreadcrumbList = () => { + const { breadcrumbList } = this.props; + const { routes, params, routerLocation, breadcrumbNameMap } = this.getBreadcrumbProps(); + if (breadcrumbList && breadcrumbList.length) { + return this.conversionFromProps(); + } + // 如果传入 routes 和 params 属性 + // If pass routes and params attributes + if (routes && params) { + return ( + route.breadcrumbName)} + params={params} + itemRender={this.itemRender} + /> + ); + } + // 根据 location 生成 面包屑 + // Generate breadcrumbs based on location + if (location && location.pathname) { + return this.conversionFromLocation(routerLocation, breadcrumbNameMap); + } + return null; + } + // 渲染Breadcrumb 子节点 + // Render the Breadcrumb child node itemRender = (route, params, routes, paths) => { const { linkElement = 'a' } = this.props; const last = routes.indexOf(route) === routes.length - 1; @@ -55,77 +128,19 @@ export default class PageHeader extends PureComponent { to: paths.join('/') || '/', }, route.breadcrumbName); } + render() { - const { routes, params, location, breadcrumbNameMap } = this.getBreadcrumbProps(); const { title, logo, action, content, extraContent, - breadcrumbList, tabList, className, linkElement = 'a', - tabActiveKey, + tabList, className, tabActiveKey, } = this.props; const clsString = classNames(styles.pageHeader, className); - let breadcrumb; - if (breadcrumbList && breadcrumbList.length) { - breadcrumb = ( - - { - breadcrumbList.map(item => ( - - {item.href ? ( - createElement(linkElement, { - [linkElement === 'a' ? 'href' : 'to']: item.href, - }, item.title) - ) : item.title} - ) - ) - } - - ); - } else if (routes && params) { - breadcrumb = ( - route.breadcrumbName)} - params={params} - itemRender={this.itemRender} - /> - ); - } else if (location && location.pathname) { - const pathSnippets = location.pathname.split('/').filter(i => i); - const extraBreadcrumbItems = pathSnippets.map((_, index) => { - const url = `/${pathSnippets.slice(0, index + 1).join('/')}`; - const currentBreadcrumb = getBreadcrumb(breadcrumbNameMap, url); - const isLinkable = (index !== pathSnippets.length - 1) && currentBreadcrumb.component; - return currentBreadcrumb.name && !currentBreadcrumb.hideInBreadcrumb ? ( - - {createElement( - isLinkable ? linkElement : 'span', - { [linkElement === 'a' ? 'href' : 'to']: url }, - currentBreadcrumb.name, - )} - - ) : null; - }); - const breadcrumbItems = [( - - {createElement(linkElement, { - [linkElement === 'a' ? 'href' : 'to']: '/', - }, '首页')} - - )].concat(extraBreadcrumbItems); - breadcrumb = ( - - {breadcrumbItems} - - ); - } else { - breadcrumb = null; - } let tabDefaultValue; if (tabActiveKey !== undefined && tabList) { tabDefaultValue = tabList.filter(item => item.default)[0] || tabList[0]; } - + const breadcrumb = this.conversionBreadcrumbList(); const activeKeyProps = { defaultActiveKey: tabDefaultValue && tabDefaultValue.key, }; diff --git a/src/components/SiderMenu/SiderMenu.js b/src/components/SiderMenu/SiderMenu.js index 71827341ccb6830869940951e9f7506db0ef65d1..279f1b2719cdbdd180d4b9aa7442cf994efa80c7 100644 --- a/src/components/SiderMenu/SiderMenu.js +++ b/src/components/SiderMenu/SiderMenu.js @@ -1,5 +1,6 @@ import React, { PureComponent } from 'react'; import { Layout, Menu, Icon } from 'antd'; +import pathToRegexp from 'path-to-regexp'; import { Link } from 'dva/router'; import styles from './index.less'; @@ -35,22 +36,43 @@ export default class SiderMenu extends PureComponent { }); } } + /** + * Convert pathname to openKeys + * /list/search/articles = > ['list','/list/search'] + * @param props + */ getDefaultCollapsedSubMenus(props) { const { location: { pathname } } = props || this.props; - const snippets = pathname.split('/').slice(1, -1); - const currentPathSnippets = snippets.map((item, index) => { - const arr = snippets.filter((_, i) => i <= index); - return arr.join('/'); + // eg. /list/search/articles = > ['','list','search','articles'] + let snippets = pathname.split('/'); + // Delete the end + // eg. delete 'articles' + snippets.pop(); + // Delete the head + // eg. delete '' + snippets.shift(); + // eg. After the operation is completed, the array should be ['list','search'] + // eg. Forward the array as ['list','list/search'] + snippets = snippets.map((item, index) => { + // If the array length > 1 + if (index > 0) { + // eg. search => ['list','search'].join('/') + return snippets.slice(0, index + 1).join('/'); + } + // index 0 to not do anything + return item; }); - let currentMenuSelectedKeys = []; - currentPathSnippets.forEach((item) => { - currentMenuSelectedKeys = currentMenuSelectedKeys.concat(this.getSelectedMenuKeys(item)); + snippets = snippets.map((item) => { + return this.getSelectedMenuKeys(`/${item}`)[0]; }); - if (currentMenuSelectedKeys.length === 0) { - return ['dashboard']; - } - return currentMenuSelectedKeys; + // eg. ['list','list/search'] + return snippets; } + /** + * Recursively flatten the data + * [{path:string},{path:string}] => {path,path2} + * @param menus + */ getFlatMenuKeys(menus) { let keys = []; menus.forEach((item) => { @@ -63,18 +85,14 @@ export default class SiderMenu extends PureComponent { }); return keys; } + /** + * Get selected child nodes + * /user/chen => /user/:id + */ getSelectedMenuKeys = (path) => { const flatMenuKeys = this.getFlatMenuKeys(this.menus); - if (flatMenuKeys.indexOf(path.replace(/^\//, '')) > -1) { - return [path.replace(/^\//, '')]; - } - if (flatMenuKeys.indexOf(path.replace(/^\//, '').replace(/\/$/, '')) > -1) { - return [path.replace(/^\//, '').replace(/\/$/, '')]; - } return flatMenuKeys.filter((item) => { - const itemRegExpStr = `^${item.replace(/:[\w-]+/g, '[\\w-]+')}$`; - const itemRegExp = new RegExp(itemRegExpStr); - return itemRegExp.test(path.replace(/^\//, '').replace(/\/$/, '')); + return pathToRegexp(`/${item}`).test(path); }); } /** @@ -120,14 +138,14 @@ export default class SiderMenu extends PureComponent { ) : item.name } - key={item.key || item.path} + key={item.path} > {this.getNavMenuItems(item.children)} ); } else { return ( - + {this.getMenuItemPath(item)} ); diff --git a/src/utils/utils.js b/src/utils/utils.js index 46a0a0f28fd364edf2c9a3424f4869ef7e5a0e7b..93fb54f4a0bcff5dcc04f3cd639f34f86aaa6fcc 100644 --- a/src/utils/utils.js +++ b/src/utils/utils.js @@ -93,6 +93,7 @@ export function digitUppercase(n) { return s.replace(/(零.)*零元/, '元').replace(/(零.)+/g, '零').replace(/^整$/, '零元整'); } + function getRelation(str1, str2) { if (str1 === str2) { console.warn('Two path are equal!'); // eslint-disable-line @@ -107,20 +108,36 @@ function getRelation(str1, str2) { return 3; } -export function getRoutes(path, routerData) { - let routes = Object.keys(routerData).filter(routePath => - routePath.indexOf(path) === 0 && routePath !== path); - routes = routes.map(item => item.replace(path, '')); +function getRenderArr(routes) { let renderArr = []; renderArr.push(routes[0]); for (let i = 1; i < routes.length; i += 1) { let isAdd = false; + // 是否包含 isAdd = renderArr.every(item => getRelation(item, routes[i]) === 3); + // 去重 renderArr = renderArr.filter(item => getRelation(item, routes[i]) !== 1); if (isAdd) { renderArr.push(routes[i]); } } + return renderArr; +} + +/** + * Get router routing configuration + * { path:{name,...param}}=>Array<{name,path ...param}> + * @param {string} path + * @param {routerData} routerData + */ +export function getRoutes(path, routerData) { + let routes = Object.keys(routerData).filter(routePath => + routePath.indexOf(path) === 0 && routePath !== path); + // Replace path to '' eg. path='user' /user/name => name + routes = routes.map(item => item.replace(path, '')); + // Get the route to be rendered to remove the deep rendering + const renderArr = getRenderArr(routes); + // Conversion and stitching parameters const renderRoutes = renderArr.map((item) => { const exact = !routes.some(route => route !== item && getRelation(route, item) === 1); return {