Unverified Commit 1abac6a3 authored by 陈小聪's avatar 陈小聪 Committed by GitHub

[v4] transform typescript (#3702)

* merge v3 to v4

* src/components/IconFont

* src/components/PageLoading

* src/components/SelectLang

* src/components/SettingDrawer

* remove e2e and test

* src/components/TopNavHeader

* src/components/GlobalHeader

* src/components/HeaderDropdown

* src/components/HeaderSearch

* src/components/TopNavHeader

* fix error

* mock

* move defaultSettings

* global.txs

* src/locales

* remove lint mock

* fix ci test error

* change PureComponent to Component, interface IDefaultSettings

* Don't prefix interface with I
Close: #3706

* strictNullChecks set true
parent e0b20cb7
// https://umijs.org/config/ // https://umijs.org/config/
import os from 'os'; import os from 'os';
import webpackPlugin from './plugin.config';
import defaultSettings from '../src/defaultSettings';
import slash from 'slash2'; import slash from 'slash2';
import { IPlugin } from 'umi-types';
import defaultSettings from './defaultSettings';
import webpackPlugin from './plugin.config';
const { pwa, primaryColor } = defaultSettings; const { pwa, primaryColor } = defaultSettings;
const { NODE_ENV, APP_TYPE, TEST } = process.env; const { APP_TYPE, TEST } = process.env;
const plugins = [ const plugins: IPlugin[] = [
[ [
'umi-plugin-react', 'umi-plugin-react',
{ {
......
module.exports = { export declare type SiderTheme = 'light' | 'dark';
export interface DefaultSettings {
navTheme: string | SiderTheme;
primaryColor: string;
layout: string;
contentWidth: string;
fixedHeader: boolean;
autoHideHeader: boolean;
fixSiderbar: boolean;
menu: { disableLocal: boolean };
title: string;
pwa: boolean;
iconfontUrl: string;
colorWeak: boolean;
}
export default {
navTheme: 'dark', // theme for nav menu navTheme: 'dark', // theme for nav menu
primaryColor: '#1890FF', // primary color of ant design primaryColor: '#1890FF', // primary color of ant design
layout: 'sidemenu', // nav menu position: sidemenu or topmenu layout: 'sidemenu', // nav menu position: sidemenu or topmenu
...@@ -6,6 +23,7 @@ module.exports = { ...@@ -6,6 +23,7 @@ module.exports = {
fixedHeader: false, // sticky header fixedHeader: false, // sticky header
autoHideHeader: false, // auto hide header autoHideHeader: false, // auto hide header
fixSiderbar: false, // sticky siderbar fixSiderbar: false, // sticky siderbar
colorWeak: false,
menu: { menu: {
disableLocal: false, disableLocal: false,
}, },
...@@ -15,4 +33,4 @@ module.exports = { ...@@ -15,4 +33,4 @@ module.exports = {
// eg://at.alicdn.com/t/font_1039637_btcrd5co4w.js // eg://at.alicdn.com/t/font_1039637_btcrd5co4w.js
// 注意:如果需要图标多色,Iconfont图标项目里要进行批量去色处理 // 注意:如果需要图标多色,Iconfont图标项目里要进行批量去色处理
iconfontUrl: '', iconfontUrl: '',
}; } as DefaultSettings;
import config from '../config/config';
const RouterConfig = config.routes;
const BASE_URL = `http://localhost:${process.env.PORT || 8000}`;
function formatter(data) {
return data
.reduce((pre, item) => {
pre.push(item.path);
return pre;
}, [])
.filter(item => item);
}
describe('Homepage', async () => {
const testPage = path => async () => {
await page.goto(`${BASE_URL}${path}`);
await page.waitForSelector('footer', {
timeout: 2000,
});
const haveFooter = await page.evaluate(
() => document.getElementsByTagName('footer').length > 0
);
expect(haveFooter).toBeTruthy();
};
beforeAll(async () => {
jest.setTimeout(1000000);
await page.setCacheEnabled(false);
});
const routers = formatter(RouterConfig[1].routes);
routers.forEach(route => {
it(`test pages ${route}`, testPage(route));
});
});
const BASE_URL = `http://localhost:${process.env.PORT || 8000}`;
describe('Homepage', () => {
beforeAll(async () => {
jest.setTimeout(1000000);
});
it('it should have logo text', async () => {
await page.goto(BASE_URL);
await page.waitForSelector('h1', {
timeout: 5000,
});
const text = await page.evaluate(() => document.getElementsByTagName('h1')[0].innerText);
expect(text).toContain('Ant Design Pro');
});
});
const BASE_URL = `http://localhost:${process.env.PORT || 8000}`;
describe('Homepage', () => {
beforeAll(async () => {
jest.setTimeout(1000000);
});
it('topmenu should have footer', async () => {
const params = '/form/basic-form?navTheme=light&layout=topmenu';
await page.goto(`${BASE_URL}${params}`);
await page.waitForSelector('footer', {
timeout: 2000,
});
const haveFooter = await page.evaluate(
() => document.getElementsByTagName('footer').length > 0
);
expect(haveFooter).toBeTruthy();
});
});
import config from '../config/config';
const RouterConfig = config.routes;
const BASE_URL = `http://localhost:${process.env.PORT || 8000}`;
function formatter(data) {
return data
.reduce((pre, item) => {
pre.push(item.path);
return pre;
}, [])
.filter(item => item);
}
describe('Homepage', () => {
const testPage = path => async () => {
await page.goto(`${BASE_URL}${path}`);
await page.waitForSelector('footer', {
timeout: 2000,
});
const haveFooter = await page.evaluate(
() => document.getElementsByTagName('footer').length > 0
);
expect(haveFooter).toBeTruthy();
};
beforeAll(async () => {
jest.setTimeout(1000000);
});
formatter(RouterConfig[0].routes).forEach(route => {
it(`test pages ${route}`, testPage(route));
});
});
...@@ -13,8 +13,8 @@ ...@@ -13,8 +13,8 @@
"analyze": "cross-env ANALYZE=1 umi build", "analyze": "cross-env ANALYZE=1 umi build",
"lint:style": "stylelint 'src/**/*.less' --syntax less", "lint:style": "stylelint 'src/**/*.less' --syntax less",
"lint:prettier": "check-prettier lint", "lint:prettier": "check-prettier lint",
"lint": "eslint --ext .js src mock tests && npm run lint:style && npm run lint:prettier", "lint": "eslint --ext .js src tests && npm run lint:style && npm run lint:prettier",
"lint:fix": "eslint --fix --ext .js src mock tests && stylelint --fix 'src/**/*.less' --syntax less", "lint:fix": "eslint --fix --ext .js src tests && stylelint --fix 'src/**/*.less' --syntax less",
"lint-staged": "lint-staged", "lint-staged": "lint-staged",
"lint-staged:js": "eslint --ext .js", "lint-staged:js": "eslint --ext .js",
"test": "umi test", "test": "umi test",
...@@ -47,9 +47,12 @@ ...@@ -47,9 +47,12 @@
"react-copy-to-clipboard": "^5.0.1", "react-copy-to-clipboard": "^5.0.1",
"react-document-title": "^2.0.3", "react-document-title": "^2.0.3",
"react-media": "^1.8.0", "react-media": "^1.8.0",
"umi-request": "^1.0.0" "react-media-hook2": "^1.0.2",
"umi-request": "^1.0.0",
"umi-types": "^0.2.0"
}, },
"devDependencies": { "devDependencies": {
"@types/jest": "^24.0.11",
"@types/react": "^16.8.1", "@types/react": "^16.8.1",
"@types/react-dom": "^16.0.11", "@types/react-dom": "^16.0.11",
"antd-pro-merge-less": "^1.0.0", "antd-pro-merge-less": "^1.0.0",
...@@ -90,8 +93,8 @@ ...@@ -90,8 +93,8 @@
"tslint-react": "^3.6.0", "tslint-react": "^3.6.0",
"umi": "^2.4.4", "umi": "^2.4.4",
"umi-plugin-ga": "^1.1.3", "umi-plugin-ga": "^1.1.3",
"umi-plugin-react": "^1.3.4", "umi-plugin-pro-block": "^1.2.0",
"umi-plugin-pro-block": "^1.2.0" "umi-plugin-react": "^1.3.4"
}, },
"optionalDependencies": { "optionalDependencies": {
"puppeteer": "^1.12.1" "puppeteer": "^1.12.1"
......
...@@ -26,7 +26,7 @@ export function patchRoutes(routes) { ...@@ -26,7 +26,7 @@ export function patchRoutes(routes) {
Object.keys(authRoutes).map(authKey => Object.keys(authRoutes).map(authKey =>
ergodicRoutes(routes, authKey, authRoutes[authKey].authority) ergodicRoutes(routes, authKey, authRoutes[authKey].authority)
); );
window.g_routes = routes; (window as any).g_routes = routes;
} }
export function render(oldRender) { export function render(oldRender) {
......
import React, { PureComponent } from 'react'; import React, { Component } from 'react';
import { FormattedMessage, formatMessage } from 'umi/locale'; import { FormattedMessage, formatMessage } from 'umi-plugin-locale';
import { Spin, Tag, Menu, Icon, Avatar, Tooltip, message } from 'antd'; import { Spin, Tag, Menu, Icon, Avatar, Tooltip, message } from 'antd';
import { ClickParam } from 'antd/es/menu';
import moment from 'moment'; import moment from 'moment';
import groupBy from 'lodash/groupBy'; import groupBy from 'lodash/groupBy';
import { NoticeIcon } from 'ant-design-pro'; import { NoticeIcon } from 'ant-design-pro';
...@@ -9,7 +10,29 @@ import HeaderDropdown from '../HeaderDropdown'; ...@@ -9,7 +10,29 @@ import HeaderDropdown from '../HeaderDropdown';
import SelectLang from '../SelectLang'; import SelectLang from '../SelectLang';
import styles from './index.less'; import styles from './index.less';
export default class GlobalHeaderRight extends PureComponent { export declare type SiderTheme = 'light' | 'dark';
interface GlobalHeaderRightProps {
notices?: any[];
dispatch?: (args: any) => void;
// wait for https://github.com/umijs/umi/pull/2036
currentUser?: {
avatar?: string;
name?: string;
title?: string;
group?: string;
signature?: string;
geographic?: any;
tags?: any[];
unreadCount: number;
};
fetchingNotices?: boolean;
onNoticeVisibleChange?: (visible: boolean) => void;
onMenuClick?: (param: ClickParam) => void;
onNoticeClear?: (tabName: string) => void;
theme?: SiderTheme;
}
export default class GlobalHeaderRight extends Component<GlobalHeaderRightProps> {
getNoticeData() { getNoticeData() {
const { notices = [] } = this.props; const { notices = [] } = this.props;
if (notices.length === 0) { if (notices.length === 0) {
...@@ -41,7 +64,7 @@ export default class GlobalHeaderRight extends PureComponent { ...@@ -41,7 +64,7 @@ export default class GlobalHeaderRight extends PureComponent {
return groupBy(newNotices, 'type'); return groupBy(newNotices, 'type');
} }
getUnreadData = noticeData => { getUnreadData: (noticeData: object) => any = noticeData => {
const unreadMsg = {}; const unreadMsg = {};
Object.entries(noticeData).forEach(([key, value]) => { Object.entries(noticeData).forEach(([key, value]) => {
if (!unreadMsg[key]) { if (!unreadMsg[key]) {
...@@ -126,25 +149,26 @@ export default class GlobalHeaderRight extends PureComponent { ...@@ -126,25 +149,26 @@ export default class GlobalHeaderRight extends PureComponent {
<Icon type="question-circle-o" /> <Icon type="question-circle-o" />
</a> </a>
</Tooltip> </Tooltip>
<NoticeIcon <NoticeIcon
className={styles.action} className={styles.action}
count={currentUser.unreadCount} count={currentUser.unreadCount}
onItemClick={(item, tabProps) => { onItemClick={(item, tabProps) => {
console.log(item, tabProps); // eslint-disable-line console.log(item, tabProps); // eslint-disable-line
this.changeReadState(item, tabProps); this.changeReadState(item);
}} }}
loading={fetchingNotices} loading={fetchingNotices}
locale={{ locale={{
emptyText: formatMessage({ id: 'component.noticeIcon.empty' }), emptyText: formatMessage({ id: 'component.noticeIcon.empty' }),
clear: formatMessage({ id: 'component.noticeIcon.clear' }), clear: formatMessage({ id: 'component.noticeIcon.clear' }),
viewMore: formatMessage({ id: 'component.noticeIcon.view-more' }), viewMore: formatMessage({ id: 'component.noticeIcon.view-more' }), // todo:node_modules/ant-design-pro/lib/NoticeIcon/index.d.ts 21 [key: string]: string;
notification: formatMessage({ id: 'component.globalHeader.notification' }), notification: formatMessage({ id: 'component.globalHeader.notification' }),
message: formatMessage({ id: 'component.globalHeader.message' }), message: formatMessage({ id: 'component.globalHeader.message' }),
event: formatMessage({ id: 'component.globalHeader.event' }), event: formatMessage({ id: 'component.globalHeader.event' }),
}} }}
onClear={onNoticeClear} onClear={onNoticeClear}
onPopupVisibleChange={onNoticeVisibleChange} onPopupVisibleChange={onNoticeVisibleChange}
onViewMore={() => message.info('Click on view more')} onViewMore={() => message.info('Click on view more')} // todo:onViewMore?: (tabProps: INoticeIconProps) => void;
clearClose clearClose
> >
<NoticeIcon.Tab <NoticeIcon.Tab
...@@ -153,7 +177,7 @@ export default class GlobalHeaderRight extends PureComponent { ...@@ -153,7 +177,7 @@ export default class GlobalHeaderRight extends PureComponent {
title="notification" title="notification"
emptyText={formatMessage({ id: 'component.globalHeader.notification.empty' })} emptyText={formatMessage({ id: 'component.globalHeader.notification.empty' })}
emptyImage="https://gw.alipayobjects.com/zos/rmsportal/wAhyIChODzsoKIOBHcBk.svg" emptyImage="https://gw.alipayobjects.com/zos/rmsportal/wAhyIChODzsoKIOBHcBk.svg"
showViewMore showViewMore // todo:showViewMore?: boolean; skeletonProps?: SkeletonProps;
/> />
<NoticeIcon.Tab <NoticeIcon.Tab
count={unreadMsg.message} count={unreadMsg.message}
......
import React, { PureComponent } from 'react'; import React, { Component } from 'react';
import { Icon } from 'antd'; import { Icon } from 'antd';
import Link from 'umi/link'; import Link from 'umi/link';
import Debounce from 'lodash-decorators/debounce'; import debounce from 'lodash/debounce';
import styles from './index.less'; import styles from './index.less';
import RightContent from './RightContent'; import RightContent from './RightContent';
export default class GlobalHeader extends PureComponent { interface GlobalHeaderProps {
collapsed?: boolean;
onCollapse?: (collapsed: boolean) => void;
isMobile?: boolean;
logo?: string;
onNoticeClear?: (type: string) => void;
onMenuClick?: ({ key: string }) => void;
onNoticeVisibleChange?: (b: boolean) => void;
}
export default class GlobalHeader extends Component<GlobalHeaderProps> {
componentWillUnmount() { componentWillUnmount() {
this.triggerResizeEvent.cancel(); this.triggerResizeEvent.cancel();
} }
/* eslint-disable*/ triggerResizeEvent = debounce(() => {
@Debounce(600)
triggerResizeEvent() {
// eslint-disable-line // eslint-disable-line
const event = document.createEvent('HTMLEvents'); const event = document.createEvent('HTMLEvents');
event.initEvent('resize', true, false); event.initEvent('resize', true, false);
window.dispatchEvent(event); window.dispatchEvent(event);
} });
toggle = () => { toggle = () => {
const { collapsed, onCollapse } = this.props; const { collapsed, onCollapse } = this.props;
onCollapse(!collapsed); onCollapse(!collapsed);
......
import * as React from 'react';
export default class HeaderDropdown extends React.Component<any, any> {}
import React, { PureComponent } from 'react'; import React, { Component } from 'react';
import { Dropdown } from 'antd'; import { Dropdown } from 'antd';
import classNames from 'classnames'; import classNames from 'classnames';
import styles from './index.less'; import styles from './index.less';
export default class HeaderDropdown extends PureComponent { declare type OverlayFunc = () => React.ReactNode;
interface HeaderDropdownProps {
overlayClassName?: string;
overlay: React.ReactNode | OverlayFunc;
placement?: 'bottomLeft' | 'bottomRight' | 'topLeft' | 'topCenter' | 'topRight' | 'bottomCenter';
}
export default class HeaderDropdown extends Component<HeaderDropdownProps> {
render() { render() {
const { overlayClassName, ...props } = this.props; const { overlayClassName, ...props } = this.props;
return ( return (
<Dropdown overlayClassName={classNames(styles.container, overlayClassName)} {...props} /> <Dropdown overlayClassName={classNames(styles.container, overlayClassName)} {...props} />
); );
......
import * as React from 'react';
export interface IHeaderSearchProps {
placeholder?: string;
dataSource?: string[];
defaultOpen?: boolean;
open?: boolean;
onSearch?: (value: string) => void;
onChange?: (value: string) => void;
onVisibleChange?: (visible: boolean) => void;
onPressEnter?: (value: string) => void;
style?: React.CSSProperties;
className?: string;
}
export default class HeaderSearch extends React.Component<IHeaderSearchProps, any> {}
import React, { PureComponent } from 'react'; import React, { Component } from 'react';
import PropTypes from 'prop-types';
import { Input, Icon, AutoComplete } from 'antd'; import { Input, Icon, AutoComplete } from 'antd';
import InputProps from 'antd/es/input';
import classNames from 'classnames'; import classNames from 'classnames';
import Debounce from 'lodash-decorators/debounce'; import Debounce from 'lodash-decorators/debounce';
import Bind from 'lodash-decorators/bind'; import Bind from 'lodash-decorators/bind';
import styles from './index.less'; import styles from './index.less';
export default class HeaderSearch extends PureComponent { interface HeaderSearchProps {
static propTypes = { onPressEnter: (value: string) => void;
className: PropTypes.string, onSearch: (value: string) => void;
placeholder: PropTypes.string, onChange: (value: string) => void;
onSearch: PropTypes.func, onVisibleChange: (b: boolean) => void;
onChange: PropTypes.func, className: string;
onPressEnter: PropTypes.func, placeholder: string;
defaultActiveFirstOption: PropTypes.bool, defaultActiveFirstOption: boolean;
dataSource: PropTypes.array, dataSource: any[];
defaultOpen: PropTypes.bool, defaultOpen: boolean;
onVisibleChange: PropTypes.func, open?: boolean;
}; }
interface HeaderSearchState {
value: string;
searchMode: boolean;
}
export default class HeaderSearch extends Component<HeaderSearchProps, HeaderSearchState> {
static defaultProps = { static defaultProps = {
defaultActiveFirstOption: false, defaultActiveFirstOption: false,
onPressEnter: () => {}, onPressEnter: () => {},
...@@ -40,6 +46,8 @@ export default class HeaderSearch extends PureComponent { ...@@ -40,6 +46,8 @@ export default class HeaderSearch extends PureComponent {
return null; return null;
} }
timeout: NodeJS.Timeout;
input: InputProps;
constructor(props) { constructor(props) {
super(props); super(props);
this.state = { this.state = {
......
import { Icon } from 'antd'; import { Icon } from 'antd';
import { iconfontUrl as scriptUrl } from '../../defaultSettings'; import defaultSettings from '../../../config/defaultSettings';
const { iconfontUrl } = defaultSettings;
const scriptUrl = iconfontUrl;
// 使用: // 使用:
// import IconFont from '@/components/IconFont'; // import IconFont from '@/components/IconFont';
// <IconFont type='icon-demo' className='xxx-xxx' /> // <IconFont type='icon-demo' className='xxx-xxx' />
......
...@@ -3,8 +3,9 @@ import { Spin } from 'antd'; ...@@ -3,8 +3,9 @@ import { Spin } from 'antd';
// loading components from code split // loading components from code split
// https://umijs.org/plugin/umi-plugin-react.html#dynamicimport // https://umijs.org/plugin/umi-plugin-react.html#dynamicimport
export default () => ( const PageLoding: React.SFC = () => (
<div style={{ paddingTop: 100, textAlign: 'center' }}> <div style={{ paddingTop: 100, textAlign: 'center' }}>
<Spin size="large" /> <Spin size="large" />
</div> </div>
); );
export default PageLoding;
import React, { PureComponent } from 'react';
import { formatMessage, setLocale, getLocale } from 'umi/locale';
import { Menu, Icon } from 'antd';
import classNames from 'classnames';
import HeaderDropdown from '../HeaderDropdown';
import styles from './index.less';
export default class SelectLang extends PureComponent {
changeLang = ({ key }) => {
setLocale(key);
};
render() {
const { className } = this.props;
const selectedLang = getLocale();
const locales = ['zh-CN', 'zh-TW', 'en-US', 'pt-BR'];
const languageLabels = {
'zh-CN': '简体中文',
'zh-TW': '繁体中文',
'en-US': 'English',
'pt-BR': 'Português',
};
const languageIcons = {
'zh-CN': '🇨🇳',
'zh-TW': '🇭🇰',
'en-US': '🇬🇧',
'pt-BR': '🇧🇷',
};
const langMenu = (
<Menu className={styles.menu} selectedKeys={[selectedLang]} onClick={this.changeLang}>
{locales.map(locale => (
<Menu.Item key={locale}>
<span role="img" aria-label={languageLabels[locale]}>
{languageIcons[locale]}
</span>{' '}
{languageLabels[locale]}
</Menu.Item>
))}
</Menu>
);
return (
<HeaderDropdown overlay={langMenu} placement="bottomRight">
<span className={classNames(styles.dropDown, className)}>
<Icon type="global" title={formatMessage({ id: 'navBar.lang' })} />
</span>
</HeaderDropdown>
);
}
}
import React from 'react';
import { formatMessage, setLocale, getLocale } from 'umi-plugin-locale';
import { Menu, Icon } from 'antd';
import classNames from 'classnames';
import HeaderDropdown from '../HeaderDropdown';
import styles from './index.less';
interface SelectLangProps {
className?: string;
}
const SelectLang: React.SFC<SelectLangProps> = props => {
const { className } = props;
const selectedLang = getLocale();
const changeLang = ({ key }) => {
setLocale(key);
};
const locales = ['zh-CN', 'zh-TW', 'en-US', 'pt-BR'];
const languageLabels = {
'zh-CN': '简体中文',
'zh-TW': '繁体中文',
'en-US': 'English',
'pt-BR': 'Português',
};
const languageIcons = {
'zh-CN': '🇨🇳',
'zh-TW': '🇭🇰',
'en-US': '🇬🇧',
'pt-BR': '🇧🇷',
};
const langMenu = (
<Menu className={styles.menu} selectedKeys={[selectedLang]} onClick={changeLang}>
{locales.map(locale => (
<Menu.Item key={locale}>
<span role="img" aria-label={languageLabels[locale]}>
{languageIcons[locale]}
</span>{' '}
{languageLabels[locale]}
</Menu.Item>
))}
</Menu>
);
return (
<HeaderDropdown overlay={langMenu} placement="bottomRight">
<span className={classNames(styles.dropDown, className)}>
<Icon type="global" title={formatMessage({ id: 'navBar.lang' })} />
</span>
</HeaderDropdown>
);
};
export default SelectLang;
...@@ -2,7 +2,12 @@ import React from 'react'; ...@@ -2,7 +2,12 @@ import React from 'react';
import { Tooltip, Icon } from 'antd'; import { Tooltip, Icon } from 'antd';
import style from './index.less'; import style from './index.less';
const BlockChecbox = ({ value, onChange, list }) => ( interface BlockChecboxProps {
value: string;
onChange: (key: string) => void;
list: any[];
}
const BlockChecbox: React.SFC<BlockChecboxProps> = ({ value, onChange, list }) => (
<div className={style.blockChecbox} key={value}> <div className={style.blockChecbox} key={value}>
{list.map(item => ( {list.map(item => (
<Tooltip title={item.title} key={item.key}> <Tooltip title={item.title} key={item.key}>
......
import React from 'react'; import React from 'react';
import { Tooltip, Icon } from 'antd'; import { Tooltip, Icon } from 'antd';
import { formatMessage } from 'umi/locale'; import { formatMessage } from 'umi-plugin-locale';
import styles from './ThemeColor.less'; import styles from './ThemeColor.less';
const Tag = ({ color, check, ...rest }) => ( interface TagProps {
color: string;
check: boolean;
className?: string;
onClick?: () => void;
}
const Tag: React.SFC<TagProps> = ({ color, check, ...rest }) => (
<div <div
{...rest} {...rest}
style={{ style={{
...@@ -14,7 +20,14 @@ const Tag = ({ color, check, ...rest }) => ( ...@@ -14,7 +20,14 @@ const Tag = ({ color, check, ...rest }) => (
</div> </div>
); );
const ThemeColor = ({ colors, title, value, onChange }) => { interface ThemeColorProps {
colors?: any[];
title?: string;
value: string;
onChange: (color: string) => void;
}
const ThemeColor: React.SFC<ThemeColorProps> = ({ colors, title, value, onChange }) => {
let colorList = colors; let colorList = colors;
if (!colors) { if (!colors) {
colorList = [ colorList = [
......
import React, { PureComponent } from 'react'; import React, { Component } from 'react';
import { Select, message, Drawer, List, Switch, Divider, Icon, Button, Alert, Tooltip } from 'antd'; import { Select, message, Drawer, List, Switch, Divider, Icon, Button, Alert, Tooltip } from 'antd';
import { formatMessage } from 'umi/locale'; import { formatMessage } from 'umi-plugin-locale';
import { CopyToClipboard } from 'react-copy-to-clipboard'; import { CopyToClipboard } from 'react-copy-to-clipboard';
import { connect } from 'dva'; import { connect } from 'dva';
import omit from 'omit.js'; import omit from 'omit.js';
import styles from './index.less'; import styles from './index.less';
import ThemeColor from './ThemeColor'; import ThemeColor from './ThemeColor';
import BlockCheckbox from './BlockCheckbox'; import BlockCheckbox from './BlockCheckbox';
import { DefaultSettings } from '../../../config/defaultSettings';
const { Option } = Select; const { Option } = Select;
interface BodyProps {
title: string;
style?: React.CSSProperties;
}
const Body = ({ children, title, style }) => ( const Body: React.SFC<BodyProps> = ({ children, title, style }) => (
<div <div
style={{ style={{
...style, ...style,
...@@ -22,8 +27,14 @@ const Body = ({ children, title, style }) => ( ...@@ -22,8 +27,14 @@ const Body = ({ children, title, style }) => (
</div> </div>
); );
interface SettingDrawerProps {
setting?: DefaultSettings;
dispatch?: (args: any) => void;
}
interface SettingDrawerState {}
@connect(({ setting }) => ({ setting })) @connect(({ setting }) => ({ setting }))
class SettingDrawer extends PureComponent { class SettingDrawer extends Component<SettingDrawerProps, SettingDrawerState> {
state = { state = {
collapse: false, collapse: false,
}; };
...@@ -120,7 +131,7 @@ class SettingDrawer extends PureComponent { ...@@ -120,7 +131,7 @@ class SettingDrawer extends PureComponent {
return ( return (
<Tooltip title={item.disabled ? item.disabledReason : ''} placement="left"> <Tooltip title={item.disabled ? item.disabledReason : ''} placement="left">
<List.Item actions={[action]}> <List.Item actions={[action]}>
<span style={{ opacity: item.disabled ? '0.5' : '' }}>{item.title}</span> <span style={{ opacity: item.disabled ? 0.5 : 1 }}>{item.title}</span>
</List.Item> </List.Item>
</Tooltip> </Tooltip>
); );
......
import React, { PureComponent } from 'react'; import IconFont from '@/components/IconFont';
import { isUrl } from '@/utils/utils';
import { Icon, Menu } from 'antd';
import classNames from 'classnames'; import classNames from 'classnames';
import { Menu, Icon } from 'antd'; import * as H from 'history';
import React, { Component } from 'react';
import Link from 'umi/link'; import Link from 'umi/link';
import { urlToList } from '../_utils/pathTools'; import { urlToList } from '../_utils/pathTools';
import { getMenuMatches } from './SiderMenuUtils';
import { isUrl } from '@/utils/utils';
import styles from './index.less'; import styles from './index.less';
import IconFont from '@/components/IconFont'; import { getMenuMatches } from './SiderMenuUtils';
const { SubMenu } = Menu; const { SubMenu } = Menu;
...@@ -28,12 +29,39 @@ const getIcon = icon => { ...@@ -28,12 +29,39 @@ const getIcon = icon => {
return icon; return icon;
}; };
export default class BaseMenu extends PureComponent { export declare type CollapseType = 'clickTrigger' | 'responsive';
export declare type SiderTheme = 'light' | 'dark';
export declare type MenuMode =
| 'vertical'
| 'vertical-left'
| 'vertical-right'
| 'horizontal'
| 'inline';
interface BaseMenuProps {
flatMenuKeys?: any[];
location?: H.Location;
onCollapse?: (collapsed: boolean, type?: CollapseType) => void;
isMobile?: boolean;
openKeys?: any;
theme?: SiderTheme;
mode?: MenuMode;
className?: string;
collapsed?: boolean;
handleOpenChange?: (openKeys: any[]) => void;
menuData?: any[];
style?: React.CSSProperties;
onOpenChange?: (openKeys: string[]) => void;
}
interface BaseMenuState {}
export default class BaseMenu extends Component<BaseMenuProps, BaseMenuState> {
/** /**
* 获得菜单子节点 * 获得菜单子节点
* @memberof SiderMenu * @memberof SiderMenu
*/ */
getNavMenuItems = menusData => { getNavMenuItems: (menusData: any[]) => any[] = menusData => {
if (!menusData) { if (!menusData) {
return []; return [];
} }
...@@ -131,6 +159,9 @@ export default class BaseMenu extends PureComponent { ...@@ -131,6 +159,9 @@ export default class BaseMenu extends PureComponent {
location: { pathname }, location: { pathname },
className, className,
collapsed, collapsed,
handleOpenChange,
style,
menuData,
} = this.props; } = this.props;
// if pathname can't match, use the nearest parent's key // if pathname can't match, use the nearest parent's key
let selectedKeys = this.getSelectedMenuKeys(pathname); let selectedKeys = this.getSelectedMenuKeys(pathname);
...@@ -143,7 +174,6 @@ export default class BaseMenu extends PureComponent { ...@@ -143,7 +174,6 @@ export default class BaseMenu extends PureComponent {
openKeys: openKeys.length === 0 ? [...selectedKeys] : openKeys, openKeys: openKeys.length === 0 ? [...selectedKeys] : openKeys,
}; };
} }
const { handleOpenChange, style, menuData } = this.props;
const cls = classNames(className, { const cls = classNames(className, {
'top-nav-menu': mode === 'horizontal', 'top-nav-menu': mode === 'horizontal',
}); });
......
import { getFlatMenuKeys } from './SiderMenuUtils';
const menu = [
{
path: '/dashboard',
children: [
{
path: '/dashboard/name',
},
],
},
{
path: '/userinfo',
children: [
{
path: '/userinfo/:id',
children: [
{
path: '/userinfo/:id/info',
},
],
},
],
},
];
const flatMenuKeys = getFlatMenuKeys(menu);
describe('test convert nested menu to flat menu', () => {
it('simple menu', () => {
expect(flatMenuKeys).toEqual([
'/dashboard',
'/dashboard/name',
'/userinfo',
'/userinfo/:id',
'/userinfo/:id/info',
]);
});
});
import React, { PureComponent, Suspense } from 'react';
import { Layout } from 'antd'; import { Layout } from 'antd';
import classNames from 'classnames'; import classNames from 'classnames';
import * as H from 'history';
import React, { Component, Suspense } from 'react';
import Link from 'umi/link'; import Link from 'umi/link';
import styles from './index.less'; import defaultSettings from '../../../config/defaultSettings';
import PageLoading from '../PageLoading'; import PageLoading from '../PageLoading';
import styles from './index.less';
import { getDefaultCollapsedSubMenus } from './SiderMenuUtils'; import { getDefaultCollapsedSubMenus } from './SiderMenuUtils';
import { title } from '../../defaultSettings';
const BaseMenu = React.lazy(() => import('./BaseMenu')); const BaseMenu = React.lazy(() => import('./BaseMenu'));
const { Sider } = Layout; const { Sider } = Layout;
const { title } = defaultSettings;
let firstMount: boolean = true;
let firstMount = true; export declare type CollapseType = 'clickTrigger' | 'responsive';
export declare type SiderTheme = 'light' | 'dark';
export default class SiderMenu extends PureComponent { interface SiderMenuProps {
constructor(props) { menuData: any[];
super(props); location?: H.Location;
this.state = { flatMenuKeys?: any[];
openKeys: getDefaultCollapsedSubMenus(props), logo?: string;
}; collapsed: boolean;
} onCollapse: (collapsed: boolean, type?: CollapseType) => void;
fixSiderbar?: boolean;
theme?: SiderTheme;
isMobile: boolean;
}
componentDidMount() { interface SiderMenuState {
firstMount = false; openKeys: any;
} flatMenuKeysLen?: number;
}
export default class SiderMenu extends Component<SiderMenuProps, SiderMenuState> {
static getDerivedStateFromProps(props, state) { static getDerivedStateFromProps(props, state) {
const { pathname, flatMenuKeysLen } = state; const { pathname, flatMenuKeysLen } = state;
if (props.location.pathname !== pathname || props.flatMenuKeys.length !== flatMenuKeysLen) { if (props.location.pathname !== pathname || props.flatMenuKeys.length !== flatMenuKeysLen) {
...@@ -35,8 +45,18 @@ export default class SiderMenu extends PureComponent { ...@@ -35,8 +45,18 @@ export default class SiderMenu extends PureComponent {
} }
return null; return null;
} }
constructor(props: SiderMenuProps) {
super(props);
this.state = {
openKeys: getDefaultCollapsedSubMenus(props),
};
}
componentDidMount() {
firstMount = false;
}
isMainMenu = key => { isMainMenu: (key: string) => boolean = key => {
const { menuData } = this.props; const { menuData } = this.props;
return menuData.some(item => { return menuData.some(item => {
if (key) { if (key) {
...@@ -46,7 +66,7 @@ export default class SiderMenu extends PureComponent { ...@@ -46,7 +66,7 @@ export default class SiderMenu extends PureComponent {
}); });
}; };
handleOpenChange = openKeys => { handleOpenChange: (openKeys: any[]) => void = openKeys => {
const moreThanOne = openKeys.filter(openKey => this.isMainMenu(openKey)).length > 1; const moreThanOne = openKeys.filter(openKey => this.isMainMenu(openKey)).length > 1;
this.setState({ this.setState({
openKeys: moreThanOne ? [openKeys.pop()] : [...openKeys], openKeys: moreThanOne ? [openKeys.pop()] : [...openKeys],
...@@ -65,7 +85,7 @@ export default class SiderMenu extends PureComponent { ...@@ -65,7 +85,7 @@ export default class SiderMenu extends PureComponent {
return ( return (
<Sider <Sider
trigger={null} trigger={null}
collapsible collapsible={true}
collapsed={collapsed} collapsed={collapsed}
breakpoint="lg" breakpoint="lg"
onCollapse={collapse => { onCollapse={collapse => {
......
...@@ -24,6 +24,7 @@ export const getMenuMatches = (flatMenuKeys, path) => ...@@ -24,6 +24,7 @@ export const getMenuMatches = (flatMenuKeys, path) =>
} }
return false; return false;
}); });
/** /**
* 获得菜单子节点 * 获得菜单子节点
* @memberof SiderMenu * @memberof SiderMenu
......
...@@ -3,7 +3,18 @@ import { Drawer } from 'antd'; ...@@ -3,7 +3,18 @@ import { Drawer } from 'antd';
import SiderMenu from './SiderMenu'; import SiderMenu from './SiderMenu';
import { getFlatMenuKeys } from './SiderMenuUtils'; import { getFlatMenuKeys } from './SiderMenuUtils';
const SiderMenuWrapper = React.memo(props => { export declare type SiderTheme = 'light' | 'dark';
interface SiderMenuProps {
isMobile: boolean;
menuData: any[];
collapsed: boolean;
logo?: string;
theme?: SiderTheme;
onCollapse: (payload: boolean) => void;
}
const SiderMenuWrapper: React.SFC<SiderMenuProps> = props => {
const { isMobile, menuData, collapsed, onCollapse } = props; const { isMobile, menuData, collapsed, onCollapse } = props;
const flatMenuKeys = getFlatMenuKeys(menuData); const flatMenuKeys = getFlatMenuKeys(menuData);
return isMobile ? ( return isMobile ? (
...@@ -21,6 +32,6 @@ const SiderMenuWrapper = React.memo(props => { ...@@ -21,6 +32,6 @@ const SiderMenuWrapper = React.memo(props => {
) : ( ) : (
<SiderMenu {...props} flatMenuKeys={flatMenuKeys} /> <SiderMenu {...props} flatMenuKeys={flatMenuKeys} />
); );
}); };
export default SiderMenuWrapper; export default React.memo(SiderMenuWrapper);
import React, { PureComponent } from 'react'; import React, { Component } from 'react';
import Link from 'umi/link'; import Link from 'umi/link';
import RightContent from '../GlobalHeader/RightContent'; import RightContent from '../GlobalHeader/RightContent';
import BaseMenu from '../SiderMenu/BaseMenu'; import BaseMenu from '../SiderMenu/BaseMenu';
import { getFlatMenuKeys } from '../SiderMenu/SiderMenuUtils'; import { getFlatMenuKeys } from '../SiderMenu/SiderMenuUtils';
import styles from './index.less'; import styles from './index.less';
import { title } from '../../defaultSettings'; import defaultSettings from '../../../config/defaultSettings';
export default class TopNavHeader extends PureComponent { export declare type CollapseType = 'clickTrigger' | 'responsive';
export declare type SiderTheme = 'light' | 'dark';
export declare type MenuMode =
| 'vertical'
| 'vertical-left'
| 'vertical-right'
| 'horizontal'
| 'inline';
const { title } = defaultSettings;
interface TopNavHeaderProps {
theme: SiderTheme;
contentWidth?: string;
menuData?: any[];
logo?: string;
mode?: MenuMode;
flatMenuKeys?: any[];
onCollapse?: (collapsed: boolean, type?: CollapseType) => void;
isMobile?: boolean;
openKeys?: any;
className?: string;
collapsed?: boolean;
handleOpenChange?: (openKeys: any[]) => void;
style?: React.CSSProperties;
onOpenChange?: (openKeys: string[]) => void;
onNoticeClear?: (type: string) => void;
onMenuClick?: ({ key: string }) => void;
onNoticeVisibleChange?: (b: boolean) => void;
}
interface TopNavHeaderState {
maxWidth: undefined | number;
}
export default class TopNavHeader extends Component<TopNavHeaderProps, TopNavHeaderState> {
state = { state = {
maxWidth: undefined, maxWidth: undefined,
}; };
maim: HTMLDivElement;
static getDerivedStateFromProps(props) { static getDerivedStateFromProps(props) {
return { return {
maxWidth: (props.contentWidth === 'Fixed' ? 1200 : window.innerWidth) - 280 - 165 - 40, maxWidth: (props.contentWidth === 'Fixed' ? 1200 : window.innerWidth) - 280 - 165 - 40,
......
import { urlToList } from './pathTools';
describe('test urlToList', () => {
it('A path', () => {
expect(urlToList('/userinfo')).toEqual(['/userinfo']);
});
it('Secondary path', () => {
expect(urlToList('/userinfo/2144')).toEqual(['/userinfo', '/userinfo/2144']);
});
it('Three paths', () => {
expect(urlToList('/userinfo/2144/addr')).toEqual([
'/userinfo',
'/userinfo/2144',
'/userinfo/2144/addr',
]);
});
});
import React from 'react'; import React from 'react';
import { notification, Button, message } from 'antd'; import { notification, Button, message } from 'antd';
import { formatMessage } from 'umi/locale'; import { formatMessage } from 'umi-plugin-locale';
import defaultSettings from './defaultSettings'; import defaultSettings from '../config/defaultSettings';
window.React = React; (window as any).React = React;
const { pwa } = defaultSettings; const { pwa } = defaultSettings;
// if pwa is true // if pwa is true
...@@ -14,7 +14,7 @@ if (pwa) { ...@@ -14,7 +14,7 @@ if (pwa) {
}); });
// Pop up a prompt on the page asking the user if they want to use the latest version // Pop up a prompt on the page asking the user if they want to use the latest version
window.addEventListener('sw.updated', e => { window.addEventListener('sw.updated', (e: CustomEvent) => {
const reloadSW = async () => { const reloadSW = async () => {
// Check if there is sw whose state is waiting in ServiceWorkerRegistration // Check if there is sw whose state is waiting in ServiceWorkerRegistration
// https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerRegistration // https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerRegistration
......
import React, { Suspense } from 'react';
import { Layout } from 'antd';
import DocumentTitle from 'react-document-title';
import { connect } from 'dva';
import { ContainerQuery } from 'react-container-query';
import classNames from 'classnames';
import Media from 'react-media';
import logo from '../assets/logo.svg';
import Footer from './Footer';
import Header from './Header';
import Context from './MenuContext';
import PageLoading from '@/components/PageLoading';
import SiderMenu from '@/components/SiderMenu';
import getPageTitle from '@/utils/getPageTitle';
import styles from './BasicLayout.less';
// lazy load SettingDrawer
const SettingDrawer = React.lazy(() => import('@/components/SettingDrawer'));
const { Content } = Layout;
const query = {
'screen-xs': {
maxWidth: 575,
},
'screen-sm': {
minWidth: 576,
maxWidth: 767,
},
'screen-md': {
minWidth: 768,
maxWidth: 991,
},
'screen-lg': {
minWidth: 992,
maxWidth: 1199,
},
'screen-xl': {
minWidth: 1200,
maxWidth: 1599,
},
'screen-xxl': {
minWidth: 1600,
},
};
class BasicLayout extends React.Component {
componentDidMount() {
const {
dispatch,
route: { routes, authority },
} = this.props;
dispatch({
type: 'user/fetchCurrent',
});
dispatch({
type: 'setting/getSetting',
});
dispatch({
type: 'menu/getMenuData',
payload: { routes, authority },
});
}
getContext() {
const { location, breadcrumbNameMap } = this.props;
return {
location,
breadcrumbNameMap,
};
}
getLayoutStyle = () => {
const { fixSiderbar, isMobile, collapsed, layout } = this.props;
if (fixSiderbar && layout !== 'topmenu' && !isMobile) {
return {
paddingLeft: collapsed ? '80px' : '256px',
};
}
return null;
};
handleMenuCollapse = collapsed => {
const { dispatch } = this.props;
dispatch({
type: 'global/changeLayoutCollapsed',
payload: collapsed,
});
};
renderSettingDrawer = () => {
// Do not render SettingDrawer in production
// unless it is deployed in preview.pro.ant.design as demo
if (process.env.NODE_ENV === 'production' && APP_TYPE !== 'site') {
return null;
}
return <SettingDrawer />;
};
render() {
const {
navTheme,
layout: PropsLayout,
children,
location: { pathname },
isMobile,
menuData,
breadcrumbNameMap,
fixedHeader,
} = this.props;
const isTop = PropsLayout === 'topmenu';
const contentStyle = !fixedHeader ? { paddingTop: 0 } : {};
const layout = (
<Layout>
{isTop && !isMobile ? null : (
<SiderMenu
logo={logo}
theme={navTheme}
onCollapse={this.handleMenuCollapse}
menuData={menuData}
isMobile={isMobile}
{...this.props}
/>
)}
<Layout
style={{
...this.getLayoutStyle(),
minHeight: '100vh',
}}
>
<Header
menuData={menuData}
handleMenuCollapse={this.handleMenuCollapse}
logo={logo}
isMobile={isMobile}
{...this.props}
/>
<Content className={styles.content} style={contentStyle}>
{children}
</Content>
<Footer />
</Layout>
</Layout>
);
return (
<React.Fragment>
<DocumentTitle title={getPageTitle(pathname, breadcrumbNameMap)}>
<ContainerQuery query={query}>
{params => (
<Context.Provider value={this.getContext()}>
<div className={classNames(params)}>{layout}</div>
</Context.Provider>
)}
</ContainerQuery>
</DocumentTitle>
<Suspense fallback={<PageLoading />}>{this.renderSettingDrawer()}</Suspense>
</React.Fragment>
);
}
}
export default connect(({ global, setting, menu: menuModel }) => ({
collapsed: global.collapsed,
layout: setting.layout,
menuData: menuModel.menuData,
breadcrumbNameMap: menuModel.breadcrumbNameMap,
...setting,
}))(props => (
<Media query="(max-width: 599px)">
{isMobile => <BasicLayout {...props} isMobile={isMobile} />}
</Media>
));
import PageLoading from '@/components/PageLoading';
import SiderMenu from '@/components/SiderMenu';
import getPageTitle from '@/utils/getPageTitle';
import { Layout } from 'antd';
import classNames from 'classnames';
import { connect } from 'dva';
import React, { Suspense, useState } from 'react';
import { ContainerQuery } from 'react-container-query';
import DocumentTitle from 'react-document-title';
import useMedia from 'react-media-hook2';
import logo from '../assets/logo.svg';
import styles from './BasicLayout.less';
import Footer from './Footer';
import Header from './Header';
import Context from './MenuContext';
// lazy load SettingDrawer
const SettingDrawer = React.lazy(() => import('@/components/SettingDrawer'));
const { Content } = Layout;
const query = {
'screen-xs': {
maxWidth: 575,
},
'screen-sm': {
minWidth: 576,
maxWidth: 767,
},
'screen-md': {
minWidth: 768,
maxWidth: 991,
},
'screen-lg': {
minWidth: 992,
maxWidth: 1199,
},
'screen-xl': {
minWidth: 1200,
maxWidth: 1599,
},
'screen-xxl': {
minWidth: 1600,
},
};
export declare type SiderTheme = 'light' | 'dark';
interface BasicLayoutProps {
dispatch: (args: any) => void;
// wait for https://github.com/umijs/umi/pull/2036
route: any;
breadcrumbNameMap: object;
fixSiderbar: boolean;
layout: string;
navTheme: SiderTheme;
menuData: any[];
fixedHeader: boolean;
location: Location;
collapsed: boolean;
}
interface BasicLayoutContext {
location: Location;
breadcrumbNameMap: object;
}
const BasicLayout: React.SFC<BasicLayoutProps> = props => {
const {
breadcrumbNameMap,
dispatch,
children,
collapsed,
fixedHeader,
fixSiderbar,
layout: PropsLayout,
location,
menuData,
navTheme,
route: { routes, authority },
} = props;
useState(() => {
dispatch({ type: 'user/fetchCurrent' });
dispatch({ type: 'setting/getSetting' });
dispatch({ type: 'menu/getMenuData', payload: { routes, authority } });
});
const isTop = PropsLayout === 'topmenu';
const contentStyle = !fixedHeader ? { paddingTop: 0 } : {};
const isMobile = useMedia({ id: 'BasicLayout', query: '(max-width: 599px)' })[0];
const hasLeftPadding = fixSiderbar && PropsLayout !== 'topmenu' && !isMobile;
const getContext = (): BasicLayoutContext => ({ location, breadcrumbNameMap });
const handleMenuCollapse = (payload: boolean) =>
dispatch({ type: 'global/changeLayoutCollapsed', payload });
// Do not render SettingDrawer in production
// unless it is deployed in preview.pro.ant.design as demo
const renderSettingDrawer = () =>
!(process.env.NODE_ENV === 'production' && APP_TYPE !== 'site') && <SettingDrawer />;
const layout = (
<Layout>
{isTop && !isMobile ? null : (
<SiderMenu
logo={logo}
theme={navTheme}
onCollapse={handleMenuCollapse}
menuData={menuData}
isMobile={isMobile}
{...props}
/>
)}
<Layout
style={{
paddingLeft: hasLeftPadding ? (collapsed ? 80 : 256) : void 0,
minHeight: '100vh',
}}
>
<Header
menuData={menuData}
handleMenuCollapse={handleMenuCollapse}
logo={logo}
isMobile={isMobile}
{...props}
/>
<Content className={styles.content} style={contentStyle}>
{children}
</Content>
<Footer />
</Layout>
</Layout>
);
return (
<React.Fragment>
<DocumentTitle title={getPageTitle(location.pathname, breadcrumbNameMap)}>
<ContainerQuery query={query}>
{params => (
<Context.Provider value={getContext()}>
<div className={classNames(params)}>{layout}</div>
</Context.Provider>
)}
</ContainerQuery>
</DocumentTitle>
<Suspense fallback={<PageLoading />}>{renderSettingDrawer()}</Suspense>
</React.Fragment>
);
};
export default connect(({ global, setting, menu: menuModel }) => ({
collapsed: global.collapsed,
layout: setting.layout,
menuData: menuModel.menuData,
breadcrumbNameMap: menuModel.breadcrumbNameMap,
...setting,
}))(BasicLayout);
import React from 'react';
export default ({ children }) => <div>{children}</div>;
import React, { ReactNode, SFC } from 'react';
interface BlankLayoutProps {
children: ReactNode;
}
const Layout: SFC<BlankLayoutProps> = ({ children }) => <div>{children}</div>;
export default Layout;
import { Icon, Layout } from 'antd';
import React, { Fragment } from 'react'; import React, { Fragment } from 'react';
import { Layout, Icon } from 'antd';
import { GlobalFooter } from 'ant-design-pro'; import { GlobalFooter } from 'ant-design-pro';
const { Footer } = Layout; const { Footer } = Layout;
......
import React, { Component } from 'react'; import GlobalHeader from '@/components/GlobalHeader';
import { formatMessage } from 'umi/locale'; import TopNavHeader from '@/components/TopNavHeader';
import { DefaultSettings } from '../../config/defaultSettings';
import { Layout, message } from 'antd'; import { Layout, message } from 'antd';
import Animate from 'rc-animate';
import { connect } from 'dva'; import { connect } from 'dva';
import Animate from 'rc-animate';
import React, { Component } from 'react';
import { formatMessage } from 'umi-plugin-locale';
import router from 'umi/router'; import router from 'umi/router';
import GlobalHeader from '@/components/GlobalHeader';
import TopNavHeader from '@/components/TopNavHeader';
import styles from './Header.less'; import styles from './Header.less';
const { Header } = Layout; const { Header } = Layout;
class HeaderView extends Component { export declare type SiderTheme = 'light' | 'dark';
state = {
visible: true,
};
static getDerivedStateFromProps(props, state) { interface HeaderViewProps {
isMobile: boolean;
collapsed: boolean;
setting: DefaultSettings;
dispatch: (args: any) => void;
autoHideHeader: boolean;
handleMenuCollapse: (args: boolean) => void;
}
interface HeaderViewState {
visible: boolean;
}
class HeaderView extends Component<HeaderViewProps, HeaderViewState> {
static getDerivedStateFromProps(props: HeaderViewProps, state: HeaderViewState) {
if (!props.autoHideHeader && !state.visible) { if (!props.autoHideHeader && !state.visible) {
return { return {
visible: true, visible: true,
...@@ -24,6 +36,14 @@ class HeaderView extends Component { ...@@ -24,6 +36,14 @@ class HeaderView extends Component {
return null; return null;
} }
state = {
visible: true,
};
ticking: boolean;
oldScrollTop: number;
componentDidMount() { componentDidMount() {
document.addEventListener('scroll', this.handScroll, { passive: true }); document.addEventListener('scroll', this.handScroll, { passive: true });
} }
...@@ -123,7 +143,7 @@ class HeaderView extends Component { ...@@ -123,7 +143,7 @@ class HeaderView extends Component {
<Header style={{ padding: 0, width }} className={fixedHeader ? styles.fixedHeader : ''}> <Header style={{ padding: 0, width }} className={fixedHeader ? styles.fixedHeader : ''}>
{isTop && !isMobile ? ( {isTop && !isMobile ? (
<TopNavHeader <TopNavHeader
theme={navTheme} theme={navTheme as SiderTheme}
mode="horizontal" mode="horizontal"
onCollapse={handleMenuCollapse} onCollapse={handleMenuCollapse}
onNoticeClear={this.handleNoticeClear} onNoticeClear={this.handleNoticeClear}
......
import { createContext } from 'react'; import { createContext } from 'react';
export default createContext(); export default createContext({});
import React, { Component, Fragment } from 'react'; import SelectLang from '@/components/SelectLang';
import { formatMessage } from 'umi/locale'; import getPageTitle from '@/utils/getPageTitle';
import { connect } from 'dva';
import Link from 'umi/link';
import { Icon } from 'antd';
import { GlobalFooter } from 'ant-design-pro'; import { GlobalFooter } from 'ant-design-pro';
import { Icon } from 'antd';
import { connect } from 'dva';
import React, { Component, Fragment } from 'react';
import DocumentTitle from 'react-document-title'; import DocumentTitle from 'react-document-title';
import SelectLang from '@/components/SelectLang'; import { formatMessage } from 'umi-plugin-locale';
import styles from './UserLayout.less'; import Link from 'umi/link';
import logo from '../assets/logo.svg'; import logo from '../assets/logo.svg';
import getPageTitle from '@/utils/getPageTitle'; import styles from './UserLayout.less';
const links = [ const links = [
{ {
...@@ -34,7 +34,15 @@ const copyright = ( ...@@ -34,7 +34,15 @@ const copyright = (
</Fragment> </Fragment>
); );
class UserLayout extends Component { interface UserLayoutProps {
dispatch: (args: any) => void;
route: any;
breadcrumbNameMap: object;
navTheme: string;
location: Location;
}
class UserLayout extends Component<UserLayoutProps> {
componentDidMount() { componentDidMount() {
const { const {
dispatch, dispatch,
......
import { queryNotices } from '@/services/user'; import { queryNotices } from '@/services/user';
import { Effect, Subscription } from 'dva';
import { Reducer } from 'redux';
export default { export interface GlobalModelState {
collapsed: boolean;
notices: any[];
}
export interface GlobalModelType {
namespace: 'global';
state: GlobalModelState;
effects: {
fetchNotices: Effect;
clearNotices: Effect;
changeNoticeReadState: Effect;
};
reducers: {
changeLayoutCollapsed: Reducer<any>;
saveNotices: Reducer<any>;
saveClearedNotices: Reducer<any>;
};
subscriptions: { setup: Subscription };
}
const GlobalModel: GlobalModelType = {
namespace: 'global', namespace: 'global',
state: { state: {
...@@ -92,10 +115,12 @@ export default { ...@@ -92,10 +115,12 @@ export default {
setup({ history }) { setup({ history }) {
// Subscribe history(url) change, trigger `load` action if pathname is `/` // Subscribe history(url) change, trigger `load` action if pathname is `/`
return history.listen(({ pathname, search }) => { return history.listen(({ pathname, search }) => {
if (typeof window.ga !== 'undefined') { if (typeof (window as any).ga !== 'undefined') {
window.ga('send', 'pageview', pathname + search); (window as any).ga('send', 'pageview', pathname + search);
} }
}); });
}, },
}, },
}; };
export default GlobalModel;
import memoizeOne from 'memoize-one';
import isEqual from 'lodash/isEqual';
import { formatMessage } from 'umi/locale';
import Authorized from '@/utils/Authorized'; import Authorized from '@/utils/Authorized';
import { menu } from '../defaultSettings'; import { Effect } from 'dva';
import isEqual from 'lodash/isEqual';
import memoizeOne from 'memoize-one';
import { Reducer } from 'redux';
import { formatMessage } from 'umi-plugin-locale';
import defaultSettings from '../../config/defaultSettings';
const { menu } = defaultSettings;
const { check } = Authorized; const { check } = Authorized;
// Conversion router to menu. // Conversion router to menu.
function formatter(data, parentAuthority, parentName) { function formatter(data: any[], parentAuthority: string[], parentName: string): any[] {
return data return data
.map(item => { .map(item => {
if (!item.name || !item.path) { if (!item.name || !item.path) {
...@@ -44,10 +46,19 @@ function formatter(data, parentAuthority, parentName) { ...@@ -44,10 +46,19 @@ function formatter(data, parentAuthority, parentName) {
const memoizeOneFormatter = memoizeOne(formatter, isEqual); const memoizeOneFormatter = memoizeOne(formatter, isEqual);
interface SubMenuItem {
children: SubMenuItem[];
hideChildrenInMenu?: boolean;
hideInMenu?: boolean;
name?: any;
component: any;
authority?: string[];
path: string;
}
/** /**
* get SubMenu or Item * get SubMenu or Item
*/ */
const getSubMenu = item => { const getSubMenu: (item: SubMenuItem) => any = item => {
// doc: add hideChildrenInMenu // doc: add hideChildrenInMenu
if (item.children && !item.hideChildrenInMenu && item.children.some(child => child.name)) { if (item.children && !item.hideChildrenInMenu && item.children.some(child => child.name)) {
return { return {
...@@ -61,23 +72,23 @@ const getSubMenu = item => { ...@@ -61,23 +72,23 @@ const getSubMenu = item => {
/** /**
* filter menuData * filter menuData
*/ */
const filterMenuData = menuData => { const filterMenuData: (menuData: SubMenuItem[]) => SubMenuItem[] = menuData => {
if (!menuData) { if (!menuData) {
return []; return [];
} }
return menuData return menuData
.filter(item => item.name && !item.hideInMenu) .filter(item => item.name && !item.hideInMenu)
.map(item => check(item.authority, getSubMenu(item))) .map(item => check(item.authority, getSubMenu(item), null))
.filter(item => item); .filter(item => item);
}; };
/** /**
* 获取面包屑映射 * 获取面包屑映射
* @param {Object} menuData 菜单配置 * @param ISubMenuItem[] menuData 菜单配置
*/ */
const getBreadcrumbNameMap = menuData => { const getBreadcrumbNameMap: (menuData: SubMenuItem[]) => object = menuData => {
const routerMap = {}; const routerMap = {};
const flattenMenuData = data => { const flattenMenuData: (data: SubMenuItem[]) => void = data => {
data.forEach(menuItem => { data.forEach(menuItem => {
if (menuItem.children) { if (menuItem.children) {
flattenMenuData(menuItem.children); flattenMenuData(menuItem.children);
...@@ -92,7 +103,23 @@ const getBreadcrumbNameMap = menuData => { ...@@ -92,7 +103,23 @@ const getBreadcrumbNameMap = menuData => {
const memoizeOneGetBreadcrumbNameMap = memoizeOne(getBreadcrumbNameMap, isEqual); const memoizeOneGetBreadcrumbNameMap = memoizeOne(getBreadcrumbNameMap, isEqual);
export default { export interface MenuModelState {
menuData: any[];
routerData: any[];
breadcrumbNameMap: object;
}
export interface MenuModelType {
namespace: 'menu';
state: MenuModelState;
effects: {
getMenuData: Effect;
};
reducers: {
save: Reducer<any>;
};
}
const MenuModel: MenuModelType = {
namespace: 'menu', namespace: 'menu',
state: { state: {
...@@ -123,3 +150,5 @@ export default { ...@@ -123,3 +150,5 @@ export default {
}, },
}, },
}; };
export default MenuModel;
import { message } from 'antd'; import { message } from 'antd';
import defaultSettings from '../defaultSettings'; import { Reducer } from 'redux';
import defaultSettings, { DefaultSettings } from '../../config/defaultSettings';
let lessNodesAppended; export interface SettingModelType {
const updateTheme = primaryColor => { namespace: 'setting';
state: DefaultSettings;
reducers: {
getSetting: Reducer<any>;
changeSetting: Reducer<any>;
};
}
let lessNodesAppended: boolean;
const updateTheme: (primaryColor?: string) => void = primaryColor => {
// Don't compile less in production! // Don't compile less in production!
if (APP_TYPE !== 'site') { if (APP_TYPE !== 'site') {
return; return;
...@@ -13,11 +23,12 @@ const updateTheme = primaryColor => { ...@@ -13,11 +23,12 @@ const updateTheme = primaryColor => {
} }
const hideMessage = message.loading('正在编译主题!', 0); const hideMessage = message.loading('正在编译主题!', 0);
function buildIt() { function buildIt() {
if (!window.less) { if (!(window as any).less) {
console.log('no less');
return; return;
} }
setTimeout(() => { setTimeout(() => {
window.less (window as any).less
.modifyVars({ .modifyVars({
'@primary-color': primaryColor, '@primary-color': primaryColor,
}) })
...@@ -59,16 +70,16 @@ const updateTheme = primaryColor => { ...@@ -59,16 +70,16 @@ const updateTheme = primaryColor => {
} }
}; };
const updateColorWeak = colorWeak => { const updateColorWeak: (colorWeak: string) => void = colorWeak => {
document.body.className = colorWeak ? 'colorWeak' : ''; document.body.className = colorWeak ? 'colorWeak' : '';
}; };
export default { const SettingModel: SettingModelType = {
namespace: 'setting', namespace: 'setting',
state: defaultSettings, state: defaultSettings,
reducers: { reducers: {
getSetting(state) { getSetting(state) {
const setting = {}; const setting: any = {};
const urlParams = new URL(window.location.href); const urlParams = new URL(window.location.href);
Object.keys(state).forEach(key => { Object.keys(state).forEach(key => {
if (urlParams.searchParams.has(key)) { if (urlParams.searchParams.has(key)) {
...@@ -121,3 +132,4 @@ export default { ...@@ -121,3 +132,4 @@ export default {
}, },
}, },
}; };
export default SettingModel;
import { query as queryUsers, queryCurrent } from '@/services/user'; import { query as queryUsers, queryCurrent } from '@/services/user';
import { Effect } from 'dva';
import { Reducer } from 'redux';
export default { export interface UserModelState {
list: any[];
currentUser: {
avatar?: string;
name?: string;
title?: string;
group?: string;
signature?: string;
geographic?: any;
tags?: any[];
unreadCount?: number;
};
}
export interface UserModelType {
namespace: 'user';
state: UserModelState;
effects: {
fetch: Effect;
fetchCurrent: Effect;
};
reducers: {
save: Reducer<any>;
saveCurrentUser: Reducer<any>;
changeNotifyCount: Reducer<any>;
};
}
const UserModel: UserModelType = {
namespace: 'user', namespace: 'user',
state: { state: {
...@@ -50,3 +80,5 @@ export default { ...@@ -50,3 +80,5 @@ export default {
}, },
}, },
}; };
export default UserModel;
import Authorized from '@/utils/Authorized';
import { connect } from 'dva';
import pathToRegexp from 'path-to-regexp';
import React from 'react'; import React from 'react';
import Redirect from 'umi/redirect'; import Redirect from 'umi/redirect';
import pathToRegexp from 'path-to-regexp'; import { UserModelState } from '../models/user';
import { connect } from 'dva';
import Authorized from '@/utils/Authorized';
function AuthComponent({ children, location, routerData, currentCuser }) { interface AuthComponentProps {
const isLogin = currentCuser && currentCuser.name; location: Location;
routerData: any[];
user: UserModelState;
}
const getRouteAuthority = (pathname, routeData) => { const AuthComponent: React.SFC<AuthComponentProps> = ({ children, location, routerData, user }) => {
const routes = routeData.slice(); // clone const { currentUser } = user;
const isLogin = currentUser && currentUser.name;
const getRouteAuthority = (path, routeData) => {
let authorities;
routeData.forEach(route => {
// match prefix
if (pathToRegexp(`${route.path}(.*)`).test(path)) {
authorities = route.authority || authorities;
const getAuthority = (routeDatas, path) => { // get children authority recursively
let authorities; if (route.routes) {
routeDatas.forEach(route => { authorities = getRouteAuthority(path, route.routes) || authorities;
// check partial route
if (pathToRegexp(`${route.path}(.*)`).test(path)) {
if (route.authority) {
authorities = route.authority;
}
// is exact route?
if (!pathToRegexp(route.path).test(path) && route.routes) {
authorities = getAuthority(route.routes, path);
}
} }
}); }
return authorities; });
}; return authorities;
return getAuthority(routes, pathname);
}; };
return ( return (
<Authorized <Authorized
...@@ -37,8 +37,9 @@ function AuthComponent({ children, location, routerData, currentCuser }) { ...@@ -37,8 +37,9 @@ function AuthComponent({ children, location, routerData, currentCuser }) {
{children} {children}
</Authorized> </Authorized>
); );
} };
export default connect(({ menu: menuModel, user: userModel }) => ({
export default connect(({ menu: menuModel, user }) => ({
routerData: menuModel.routerData, routerData: menuModel.routerData,
currentCuser: userModel.currentCuser, user,
}))(AuthComponent); }))(AuthComponent);
import request from '@/utils/request'; import request from '@/utils/request';
export async function query() { export async function query(): Promise<any> {
return request('/api/users'); return request('/api/users');
} }
export async function queryCurrent() { export async function queryCurrent(): Promise<any> {
return request('/api/currentUser'); return request('/api/currentUser');
} }
export async function queryNotices() { export async function queryNotices(): Promise<any> {
return request('/api/notices'); return request('/api/notices');
} }
declare module '*.css';
declare module '*.less';
declare module '*.scss';
declare module '*.sass';
declare module '*.svg';
declare module '*.png';
declare module '*.jpg';
declare module '*.jpeg';
declare module '*.gif';
declare module '*.bmp';
declare module '*.tiff';
declare var APP_TYPE: string;
import 'jest';
import { getAuthority } from './authority'; import { getAuthority } from './authority';
describe('getAuthority should be strong', () => { describe('getAuthority should be strong', () => {
......
// use localStorage to store the authority info, which might be sent from server in actual project. // use localStorage to store the authority info, which might be sent from server in actual project.
export function getAuthority(str) { export function getAuthority(str?: string): any {
// return localStorage.getItem('antd-pro-authority') || ['admin', 'user']; // return localStorage.getItem('antd-pro-authority') || ['admin', 'user'];
const authorityString = const authorityString =
typeof str === 'undefined' ? localStorage.getItem('antd-pro-authority') : str; typeof str === 'undefined' ? localStorage.getItem('antd-pro-authority') : str;
...@@ -16,7 +16,7 @@ export function getAuthority(str) { ...@@ -16,7 +16,7 @@ export function getAuthority(str) {
return authority || ['admin']; return authority || ['admin'];
} }
export function setAuthority(authority) { export function setAuthority(authority: string | string[]): void {
const proAuthority = typeof authority === 'string' ? [authority] : authority; const proAuthority = typeof authority === 'string' ? [authority] : authority;
return localStorage.setItem('antd-pro-authority', JSON.stringify(proAuthority)); return localStorage.setItem('antd-pro-authority', JSON.stringify(proAuthority));
} }
import { formatMessage } from 'umi/locale';
import pathToRegexp from 'path-to-regexp';
import isEqual from 'lodash/isEqual'; import isEqual from 'lodash/isEqual';
import memoizeOne from 'memoize-one'; import memoizeOne from 'memoize-one';
import { menu, title } from '../defaultSettings'; import pathToRegexp from 'path-to-regexp';
import { formatMessage } from 'umi-plugin-locale';
import defaultSettings from '../../config/defaultSettings';
const { menu, title } = defaultSettings;
interface RouterData {
name: string;
locale: string;
authority?: string[];
children?: any[];
icon?: string;
path: string;
}
export const matchParamsPath = (pathname, breadcrumbNameMap) => { export const matchParamsPath = (pathname: string, breadcrumbNameMap: object): RouterData => {
const pathKey = Object.keys(breadcrumbNameMap).find(key => pathToRegexp(key).test(pathname)); const pathKey = Object.keys(breadcrumbNameMap).find(key => pathToRegexp(key).test(pathname));
return breadcrumbNameMap[pathKey]; return breadcrumbNameMap[pathKey];
}; };
const getPageTitle = (pathname, breadcrumbNameMap) => { const getPageTitle = (pathname: string, breadcrumbNameMap: object): string => {
const currRouterData = matchParamsPath(pathname, breadcrumbNameMap); const currRouterData = matchParamsPath(pathname, breadcrumbNameMap);
if (!currRouterData) { if (!currRouterData) {
return title; return title;
......
import { isUrl } from './utils';
describe('isUrl tests', () => {
it('should return false for invalid and corner case inputs', () => {
expect(isUrl([])).toBeFalsy();
expect(isUrl({})).toBeFalsy();
expect(isUrl(false)).toBeFalsy();
expect(isUrl(true)).toBeFalsy();
expect(isUrl(NaN)).toBeFalsy();
expect(isUrl(null)).toBeFalsy();
expect(isUrl(undefined)).toBeFalsy();
expect(isUrl()).toBeFalsy();
expect(isUrl('')).toBeFalsy();
});
it('should return false for invalid URLs', () => {
expect(isUrl('foo')).toBeFalsy();
expect(isUrl('bar')).toBeFalsy();
expect(isUrl('bar/test')).toBeFalsy();
expect(isUrl('http:/example.com/')).toBeFalsy();
expect(isUrl('ttp://example.com/')).toBeFalsy();
});
it('should return true for valid URLs', () => {
expect(isUrl('http://example.com/')).toBeTruthy();
expect(isUrl('https://example.com/')).toBeTruthy();
expect(isUrl('http://example.com/test/123')).toBeTruthy();
expect(isUrl('https://example.com/test/123')).toBeTruthy();
expect(isUrl('http://example.com/test/123?foo=bar')).toBeTruthy();
expect(isUrl('https://example.com/test/123?foo=bar')).toBeTruthy();
expect(isUrl('http://www.example.com/')).toBeTruthy();
expect(isUrl('https://www.example.com/')).toBeTruthy();
expect(isUrl('http://www.example.com/test/123')).toBeTruthy();
expect(isUrl('https://www.example.com/test/123')).toBeTruthy();
expect(isUrl('http://www.example.com/test/123?foo=bar')).toBeTruthy();
expect(isUrl('https://www.example.com/test/123?foo=bar')).toBeTruthy();
});
});
...@@ -9,18 +9,19 @@ ...@@ -9,18 +9,19 @@
"jsx": "react", "jsx": "react",
"allowSyntheticDefaultImports": true, "allowSyntheticDefaultImports": true,
"moduleResolution": "node", "moduleResolution": "node",
"rootDirs": ["/src", "/test", "/mock","./typings"], "rootDirs": ["/src", "/test", "/mock", "./typings"],
"forceConsistentCasingInFileNames": true, "forceConsistentCasingInFileNames": true,
"noImplicitReturns": true, "noImplicitReturns": true,
"suppressImplicitAnyIndexErrors": true, "suppressImplicitAnyIndexErrors": true,
"noUnusedLocals": true, "noUnusedLocals": true,
"allowJs": true, "allowJs": true,
"experimentalDecorators": true, "experimentalDecorators": true,
"strictNullChecks": true,
"paths": { "paths": {
"@/*": ["./src/*"] "@/*": ["./src/*"]
} }
}, },
"include": ["./src"], "include": ["./src", "config/defaultSettings.ts"],
"exclude": [ "exclude": [
"node_modules", "node_modules",
"build", "build",
......
...@@ -6,6 +6,8 @@ ...@@ -6,6 +6,8 @@
"object-literal-sort-keys": false, "object-literal-sort-keys": false,
"jsx-no-lambda": false, "jsx-no-lambda": false,
"no-implicit-dependencies": false, "no-implicit-dependencies": false,
"no-console": false "no-console": false,
"member-access": false,
"prefer-conditional-expression": false
} }
} }
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment