Commit 0bc812b7 authored by 何乐's avatar 何乐 Committed by 陈小聪

[v4] Strict ts (#3723)

* short-term use of tslint

* fix lint error

* connect & dispatch definition

* replace `SFC` with `FunctionComponent`

* SiderMenu

* TopNavHeader

* HeaderSearch

* components

* layouts

* layouts

* pages

* fix authorize fail

* SettingDrawer

* remove one `as any`
parent 1abac6a3
...@@ -35,5 +35,7 @@ module.exports = { ...@@ -35,5 +35,7 @@ module.exports = {
}, },
settings: { settings: {
polyfills: ['fetch', 'promises', 'url'], polyfills: ['fetch', 'promises', 'url'],
// support import modules from TypeScript files in JavaScript files
'import/resolver': { node: { extensions: ['.js', '.ts', '.tsx'] } },
}, },
}; };
{ {
"singleQuote": true, "singleQuote": true,
"trailingComma": "es5", "trailingComma": "all",
"printWidth": 100, "printWidth": 100,
"overrides": [ "overrides": [
{ {
......
// https://umijs.org/config/ // https://umijs.org/config/
import os from 'os'; // import os from 'os';
import slash from 'slash2'; import slash from 'slash2';
import { IPlugin } from 'umi-types'; import { IPlugin, IConfig } from 'umi-types';
import defaultSettings from './defaultSettings'; import defaultSettings from './defaultSettings';
import webpackPlugin from './plugin.config'; import webpackPlugin from './plugin.config';
...@@ -144,6 +144,5 @@ export default { ...@@ -144,6 +144,5 @@ export default {
manifest: { manifest: {
basePath: '/', basePath: '/',
}, },
chainWebpack: webpackPlugin, chainWebpack: webpackPlugin,
}; } as IConfig;
export declare type SiderTheme = 'light' | 'dark'; import { MenuTheme } from 'antd/es/menu';
export type ContentWidth = 'Fluid' | 'Fixed';
export interface DefaultSettings { export interface DefaultSettings {
navTheme: string | SiderTheme; /**
* theme for nav menu
*/
navTheme: MenuTheme;
/**
* primary color of ant design
*/
primaryColor: string; primaryColor: string;
layout: string; /**
contentWidth: string; * nav menu position: `sidemenu` or `topmenu`
*/
layout: 'sidemenu' | 'topmenu';
/**
* layout of content: `Fluid` or `Fixed`, only works when layout is topmenu
*/
contentWidth: ContentWidth;
/**
* sticky header
*/
fixedHeader: boolean; fixedHeader: boolean;
/**
* auto hide header
*/
autoHideHeader: boolean; autoHideHeader: boolean;
/**
* sticky siderbar
*/
fixSiderbar: boolean; fixSiderbar: boolean;
menu: { disableLocal: boolean }; menu: { disableLocal: boolean };
title: string; title: string;
pwa: boolean; pwa: boolean;
/**
* your iconfont Symbol Scrip Url
* eg:`//at.alicdn.com/t/font_1039637_btcrd5co4w.js`
* 注意:如果需要图标多色,Iconfont图标项目里要进行批量去色处理
*/
iconfontUrl: string; iconfontUrl: string;
colorWeak: boolean; colorWeak: boolean;
} }
export default { export default {
navTheme: 'dark', // theme for nav menu navTheme: 'dark',
primaryColor: '#1890FF', // primary color of ant design primaryColor: '#1890FF',
layout: 'sidemenu', // nav menu position: sidemenu or topmenu layout: 'sidemenu',
contentWidth: 'Fluid', // layout of content: Fluid or Fixed, only works when layout is topmenu contentWidth: 'Fluid',
fixedHeader: false, // sticky header fixedHeader: false,
autoHideHeader: false, // auto hide header autoHideHeader: false,
fixSiderbar: false, // sticky siderbar fixSiderbar: false,
colorWeak: false, colorWeak: false,
menu: { menu: {
disableLocal: false, disableLocal: false,
}, },
title: 'Ant Design Pro', title: 'Ant Design Pro',
pwa: true, pwa: true,
// your iconfont Symbol Scrip Url
// eg://at.alicdn.com/t/font_1039637_btcrd5co4w.js
// 注意:如果需要图标多色,Iconfont图标项目里要进行批量去色处理
iconfontUrl: '', iconfontUrl: '',
} as DefaultSettings; } as DefaultSettings;
...@@ -11,12 +11,15 @@ ...@@ -11,12 +11,15 @@
"dev:no-mock": "cross-env MOCK=none umi dev", "dev:no-mock": "cross-env MOCK=none umi dev",
"build": "umi build", "build": "umi build",
"analyze": "cross-env ANALYZE=1 umi build", "analyze": "cross-env ANALYZE=1 umi build",
"lint:js": "eslint --ext .js src tests",
"lint:ts": "tslint -p . -c tslint.yml",
"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 tests && npm run lint:style && npm run lint:prettier", "lint": "npm run lint:js && npm run lint:ts && npm run lint:style && npm run lint:prettier",
"lint:fix": "eslint --fix --ext .js src tests && stylelint --fix 'src/**/*.less' --syntax less", "lint:fix": "eslint --fix --ext .js src tests && tslint --fix -p . -c tslint.yml && 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",
"lint-staged:ts": "tslint",
"test": "umi test", "test": "umi test",
"test:component": "umi test ./src/components", "test:component": "umi test ./src/components",
"test:all": "node ./tests/run-tests.js", "test:all": "node ./tests/run-tests.js",
...@@ -46,14 +49,17 @@ ...@@ -46,14 +49,17 @@
"react-container-query": "^0.11.0", "react-container-query": "^0.11.0",
"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-hook2": "^1.0.2", "react-media-hook2": "^1.0.2",
"umi-request": "^1.0.0", "umi-request": "^1.0.0"
"umi-types": "^0.2.0"
}, },
"devDependencies": { "devDependencies": {
"@types/classnames": "^2.2.7",
"@types/enzyme": "^3.9.0",
"@types/jest": "^24.0.11", "@types/jest": "^24.0.11",
"@types/lodash": "^4.14.122",
"@types/memoize-one": "^4.1.0",
"@types/react": "^16.8.1", "@types/react": "^16.8.1",
"@types/react-document-title": "^2.0.3",
"@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",
"antd-theme-webpack-plugin": "^1.2.0", "antd-theme-webpack-plugin": "^1.2.0",
...@@ -90,11 +96,13 @@ ...@@ -90,11 +96,13 @@
"stylelint-order": "^2.0.0", "stylelint-order": "^2.0.0",
"tslint": "^5.12.1", "tslint": "^5.12.1",
"tslint-config-prettier": "^1.17.0", "tslint-config-prettier": "^1.17.0",
"tslint-eslint-rules": "^5.4.0",
"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-pro-block": "^1.2.0", "umi-plugin-pro-block": "^1.2.0",
"umi-plugin-react": "^1.3.4" "umi-plugin-react": "^1.3.4",
"umi-types": "^0.2.0"
}, },
"optionalDependencies": { "optionalDependencies": {
"puppeteer": "^1.12.1" "puppeteer": "^1.12.1"
...@@ -105,6 +113,7 @@ ...@@ -105,6 +113,7 @@
"git add" "git add"
], ],
"**/*.{js,jsx}": "npm run lint-staged:js", "**/*.{js,jsx}": "npm run lint-staged:js",
"**/*.{ts,tsx}": "npm run lint-staged:ts",
"**/*.less": "stylelint --syntax less" "**/*.less": "stylelint --syntax less"
}, },
"engines": { "engines": {
......
import fetch from 'dva/fetch'; import fetch from 'dva/fetch';
import { IRoute } from 'umi-types';
export const dva = { export const dva = {
config: { config: {
onError(err) { onError(err: ErrorEvent) {
err.preventDefault(); err.preventDefault();
}, },
}, },
...@@ -10,7 +11,7 @@ export const dva = { ...@@ -10,7 +11,7 @@ export const dva = {
let authRoutes = {}; let authRoutes = {};
function ergodicRoutes(routes, authKey, authority) { function ergodicRoutes(routes: IRoute[], authKey: string, authority: string | string[]) {
routes.forEach(element => { routes.forEach(element => {
if (element.path === authKey) { if (element.path === authKey) {
if (!element.authority) element.authority = []; // eslint-disable-line if (!element.authority) element.authority = []; // eslint-disable-line
...@@ -22,14 +23,14 @@ function ergodicRoutes(routes, authKey, authority) { ...@@ -22,14 +23,14 @@ function ergodicRoutes(routes, authKey, authority) {
}); });
} }
export function patchRoutes(routes) { export function patchRoutes(routes: IRoute[]) {
Object.keys(authRoutes).map(authKey => Object.keys(authRoutes).map(authKey =>
ergodicRoutes(routes, authKey, authRoutes[authKey].authority) ergodicRoutes(routes, authKey, authRoutes[authKey].authority),
); );
(window as any).g_routes = routes; (window as any).g_routes = routes;
} }
export function render(oldRender) { export function render(oldRender: Function) {
fetch('/api/auth_routes') fetch('/api/auth_routes')
.then(res => res.json()) .then(res => res.json())
.then( .then(
...@@ -39,6 +40,6 @@ export function render(oldRender) { ...@@ -39,6 +40,6 @@ export function render(oldRender) {
}, },
() => { () => {
oldRender(); oldRender();
} },
); );
} }
import { ConnectProps } from '@/models/connect';
import { NoticeItem } from '@/models/global';
import { CurrentUser } from '@/models/user';
import React, { Component } from 'react'; import React, { Component } from 'react';
import { FormattedMessage, formatMessage } from 'umi-plugin-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';
...@@ -10,30 +13,20 @@ import HeaderDropdown from '../HeaderDropdown'; ...@@ -10,30 +13,20 @@ import HeaderDropdown from '../HeaderDropdown';
import SelectLang from '../SelectLang'; import SelectLang from '../SelectLang';
import styles from './index.less'; import styles from './index.less';
export declare type SiderTheme = 'light' | 'dark'; export type SiderTheme = 'light' | 'dark';
interface GlobalHeaderRightProps { export interface GlobalHeaderRightProps extends ConnectProps {
notices?: any[]; notices?: NoticeItem[];
dispatch?: (args: any) => void; currentUser?: CurrentUser;
// 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; fetchingNotices?: boolean;
onNoticeVisibleChange?: (visible: boolean) => void; onNoticeVisibleChange?: (visible: boolean) => void;
onMenuClick?: (param: ClickParam) => void; onMenuClick?: (param: ClickParam) => void;
onNoticeClear?: (tabName: string) => void; onNoticeClear?: (tabName: string) => void;
theme?: SiderTheme; theme?: SiderTheme;
} }
export default class GlobalHeaderRight extends Component<GlobalHeaderRightProps> { export default class GlobalHeaderRight extends Component<GlobalHeaderRightProps> {
getNoticeData() { getNoticeData = (): { [key: string]: NoticeItem[] } => {
const { notices = [] } = this.props; const { notices = [] } = this.props;
if (notices.length === 0) { if (notices.length === 0) {
return {}; return {};
...@@ -41,7 +34,7 @@ export default class GlobalHeaderRight extends Component<GlobalHeaderRightProps> ...@@ -41,7 +34,7 @@ export default class GlobalHeaderRight extends Component<GlobalHeaderRightProps>
const newNotices = notices.map(notice => { const newNotices = notices.map(notice => {
const newNotice = { ...notice }; const newNotice = { ...notice };
if (newNotice.datetime) { if (newNotice.datetime) {
newNotice.datetime = moment(notice.datetime).fromNow(); newNotice.datetime = moment(notice.datetime as string).fromNow();
} }
if (newNotice.id) { if (newNotice.id) {
newNotice.key = newNotice.id; newNotice.key = newNotice.id;
...@@ -62,10 +55,10 @@ export default class GlobalHeaderRight extends Component<GlobalHeaderRightProps> ...@@ -62,10 +55,10 @@ export default class GlobalHeaderRight extends Component<GlobalHeaderRightProps>
return newNotice; return newNotice;
}); });
return groupBy(newNotices, 'type'); return groupBy(newNotices, 'type');
} };
getUnreadData: (noticeData: object) => any = noticeData => { getUnreadData = (noticeData: { [key: string]: NoticeItem[] }) => {
const unreadMsg = {}; const unreadMsg: { [key: string]: number } = {};
Object.entries(noticeData).forEach(([key, value]) => { Object.entries(noticeData).forEach(([key, value]) => {
if (!unreadMsg[key]) { if (!unreadMsg[key]) {
unreadMsg[key] = 0; unreadMsg[key] = 0;
...@@ -77,10 +70,10 @@ export default class GlobalHeaderRight extends Component<GlobalHeaderRightProps> ...@@ -77,10 +70,10 @@ export default class GlobalHeaderRight extends Component<GlobalHeaderRightProps>
return unreadMsg; return unreadMsg;
}; };
changeReadState = clickedItem => { changeReadState = (clickedItem: NoticeItem) => {
const { id } = clickedItem; const { id } = clickedItem;
const { dispatch } = this.props; const { dispatch } = this.props;
dispatch({ dispatch!({
type: 'global/changeNoticeReadState', type: 'global/changeNoticeReadState',
payload: id, payload: id,
}); });
...@@ -133,10 +126,10 @@ export default class GlobalHeaderRight extends Component<GlobalHeaderRightProps> ...@@ -133,10 +126,10 @@ export default class GlobalHeaderRight extends Component<GlobalHeaderRightProps>
formatMessage({ id: 'component.globalHeader.search.example3' }), formatMessage({ id: 'component.globalHeader.search.example3' }),
]} ]}
onSearch={value => { onSearch={value => {
console.log('input', value); // eslint-disable-line console.log('input', value); // tslint:disable-line no-console
}} }}
onPressEnter={value => { onPressEnter={value => {
console.log('enter', value); // eslint-disable-line console.log('enter', value); // tslint:disable-line no-console
}} }}
/> />
<Tooltip title={formatMessage({ id: 'component.globalHeader.help' })}> <Tooltip title={formatMessage({ id: 'component.globalHeader.help' })}>
...@@ -152,23 +145,23 @@ export default class GlobalHeaderRight extends Component<GlobalHeaderRightProps> ...@@ -152,23 +145,23 @@ export default class GlobalHeaderRight extends Component<GlobalHeaderRightProps>
<NoticeIcon <NoticeIcon
className={styles.action} className={styles.action}
count={currentUser.unreadCount} count={currentUser && currentUser.unreadCount}
onItemClick={(item, tabProps) => { onItemClick={(item, tabProps) => {
console.log(item, tabProps); // eslint-disable-line console.log(item, tabProps); // tslint:disable-line no-console
this.changeReadState(item); this.changeReadState(item as NoticeItem);
}} }}
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' }), // todo:node_modules/ant-design-pro/lib/NoticeIcon/index.d.ts 21 [key: string]: string; viewMore: formatMessage({ id: 'component.noticeIcon.view-more' }),
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')} // todo:onViewMore?: (tabProps: INoticeIconProps) => void; onViewMore={() => message.info('Click on view more')}
clearClose clearClose
> >
<NoticeIcon.Tab <NoticeIcon.Tab
...@@ -196,7 +189,7 @@ export default class GlobalHeaderRight extends Component<GlobalHeaderRightProps> ...@@ -196,7 +189,7 @@ export default class GlobalHeaderRight extends Component<GlobalHeaderRightProps>
showViewMore showViewMore
/> />
</NoticeIcon> </NoticeIcon>
{currentUser.name ? ( {currentUser && currentUser.name ? (
<HeaderDropdown overlay={menu}> <HeaderDropdown overlay={menu}>
<span className={`${styles.action} ${styles.account}`}> <span className={`${styles.action} ${styles.account}`}>
<Avatar <Avatar
......
...@@ -3,31 +3,35 @@ import { Icon } from 'antd'; ...@@ -3,31 +3,35 @@ import { Icon } from 'antd';
import Link from 'umi/link'; import Link from 'umi/link';
import debounce from 'lodash/debounce'; import debounce from 'lodash/debounce';
import styles from './index.less'; import styles from './index.less';
import RightContent from './RightContent'; import RightContent, { GlobalHeaderRightProps } from './RightContent';
interface GlobalHeaderProps { type PartialGlobalHeaderRightProps = {
[K in
| 'onMenuClick'
| 'onNoticeClear'
| 'onNoticeVisibleChange'
| 'currentUser']?: GlobalHeaderRightProps[K]
};
export interface GlobalHeaderProps extends PartialGlobalHeaderRightProps {
collapsed?: boolean; collapsed?: boolean;
onCollapse?: (collapsed: boolean) => void; onCollapse?: (collapsed: boolean) => void;
isMobile?: boolean; isMobile?: boolean;
logo?: string; logo?: string;
onNoticeClear?: (type: string) => void;
onMenuClick?: ({ key: string }) => void;
onNoticeVisibleChange?: (b: boolean) => void;
} }
export default class GlobalHeader extends Component<GlobalHeaderProps> { export default class GlobalHeader extends Component<GlobalHeaderProps> {
componentWillUnmount() {
this.triggerResizeEvent.cancel();
}
triggerResizeEvent = debounce(() => { triggerResizeEvent = debounce(() => {
// 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);
}); });
componentWillUnmount() {
this.triggerResizeEvent.cancel();
}
toggle = () => { toggle = () => {
const { collapsed, onCollapse } = this.props; const { collapsed, onCollapse } = this.props;
onCollapse(!collapsed); if (onCollapse) onCollapse(!collapsed);
this.triggerResizeEvent(); this.triggerResizeEvent();
}; };
render() { render() {
......
import React, { Component } from 'react'; import React from 'react';
import { Dropdown } from 'antd'; import { Dropdown } from 'antd';
import { DropDownProps } from 'antd/es/dropdown';
import classNames from 'classnames'; import classNames from 'classnames';
import styles from './index.less'; import styles from './index.less';
declare type OverlayFunc = () => React.ReactNode; export interface HeaderDropdownProps extends DropDownProps {
interface HeaderDropdownProps {
overlayClassName?: string; overlayClassName?: string;
overlay: React.ReactNode | OverlayFunc;
placement?: 'bottomLeft' | 'bottomRight' | 'topLeft' | 'topCenter' | 'topRight' | 'bottomCenter';
} }
export default class HeaderDropdown extends Component<HeaderDropdownProps> { const HeaderDropdown: React.FC<HeaderDropdownProps> = ({ overlayClassName: cls, ...restProps }) => (
render() { <Dropdown overlayClassName={classNames(styles.container, cls)} {...restProps} />
const { overlayClassName, ...props } = this.props; );
return ( export default HeaderDropdown;
<Dropdown overlayClassName={classNames(styles.container, overlayClassName)} {...props} />
);
}
}
import React, { Component } from 'react'; import React, { Component } from 'react';
import { Input, Icon, AutoComplete } from 'antd'; import { Input, Icon, AutoComplete } from 'antd';
import InputProps from 'antd/es/input'; import { DataSourceItemType } from 'antd/es/auto-complete';
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';
interface HeaderSearchProps { export interface HeaderSearchProps {
onPressEnter: (value: string) => void; onPressEnter: (value: string) => void;
onSearch: (value: string) => void; onSearch: (value: string) => void;
onChange: (value: string) => void; onChange: (value: string) => void;
...@@ -15,7 +14,7 @@ interface HeaderSearchProps { ...@@ -15,7 +14,7 @@ interface HeaderSearchProps {
className: string; className: string;
placeholder: string; placeholder: string;
defaultActiveFirstOption: boolean; defaultActiveFirstOption: boolean;
dataSource: any[]; dataSource: DataSourceItemType[];
defaultOpen: boolean; defaultOpen: boolean;
open?: boolean; open?: boolean;
} }
...@@ -24,6 +23,7 @@ interface HeaderSearchState { ...@@ -24,6 +23,7 @@ interface HeaderSearchState {
value: string; value: string;
searchMode: boolean; searchMode: boolean;
} }
export default class HeaderSearch extends Component<HeaderSearchProps, HeaderSearchState> { export default class HeaderSearch extends Component<HeaderSearchProps, HeaderSearchState> {
static defaultProps = { static defaultProps = {
defaultActiveFirstOption: false, defaultActiveFirstOption: false,
...@@ -37,7 +37,7 @@ export default class HeaderSearch extends Component<HeaderSearchProps, HeaderSea ...@@ -37,7 +37,7 @@ export default class HeaderSearch extends Component<HeaderSearchProps, HeaderSea
onVisibleChange: () => {}, onVisibleChange: () => {},
}; };
static getDerivedStateFromProps(props) { static getDerivedStateFromProps(props: HeaderSearchProps) {
if ('open' in props) { if ('open' in props) {
return { return {
searchMode: props.open, searchMode: props.open,
...@@ -46,9 +46,10 @@ export default class HeaderSearch extends Component<HeaderSearchProps, HeaderSea ...@@ -46,9 +46,10 @@ export default class HeaderSearch extends Component<HeaderSearchProps, HeaderSea
return null; return null;
} }
timeout: NodeJS.Timeout; private timeout: NodeJS.Timeout = null!;
input: InputProps; private inputRef: Input | null = null;
constructor(props) {
constructor(props: HeaderSearchProps) {
super(props); super(props);
this.state = { this.state = {
searchMode: props.defaultOpen, searchMode: props.defaultOpen,
...@@ -60,7 +61,7 @@ export default class HeaderSearch extends Component<HeaderSearchProps, HeaderSea ...@@ -60,7 +61,7 @@ export default class HeaderSearch extends Component<HeaderSearchProps, HeaderSea
clearTimeout(this.timeout); clearTimeout(this.timeout);
} }
onKeyDown = e => { onKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter') { if (e.key === 'Enter') {
const { onPressEnter } = this.props; const { onPressEnter } = this.props;
const { value } = this.state; const { value } = this.state;
...@@ -70,7 +71,7 @@ export default class HeaderSearch extends Component<HeaderSearchProps, HeaderSea ...@@ -70,7 +71,7 @@ export default class HeaderSearch extends Component<HeaderSearchProps, HeaderSea
} }
}; };
onChange = value => { onChange = (value: string) => {
const { onSearch, onChange } = this.props; const { onSearch, onChange } = this.props;
this.setState({ value }); this.setState({ value });
if (onSearch) { if (onSearch) {
...@@ -86,8 +87,8 @@ export default class HeaderSearch extends Component<HeaderSearchProps, HeaderSea ...@@ -86,8 +87,8 @@ export default class HeaderSearch extends Component<HeaderSearchProps, HeaderSea
onVisibleChange(true); onVisibleChange(true);
this.setState({ searchMode: true }, () => { this.setState({ searchMode: true }, () => {
const { searchMode } = this.state; const { searchMode } = this.state;
if (searchMode) { if (searchMode && this.inputRef) {
this.input.focus(); this.inputRef.focus();
} }
}); });
}; };
...@@ -135,11 +136,11 @@ export default class HeaderSearch extends Component<HeaderSearchProps, HeaderSea ...@@ -135,11 +136,11 @@ export default class HeaderSearch extends Component<HeaderSearchProps, HeaderSea
{...restProps} {...restProps}
className={inputClass} className={inputClass}
value={value} value={value}
onChange={this.onChange} onChange={this.onChange as any}
> >
<Input <Input
ref={node => { ref={node => {
this.input = node; this.inputRef = node;
}} }}
aria-label={placeholder} aria-label={placeholder}
placeholder={placeholder} placeholder={placeholder}
......
...@@ -3,7 +3,7 @@ import { Spin } from 'antd'; ...@@ -3,7 +3,7 @@ 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
const PageLoding: React.SFC = () => ( const PageLoding: React.FC = () => (
<div style={{ paddingTop: 100, textAlign: 'center' }}> <div style={{ paddingTop: 100, textAlign: 'center' }}>
<Spin size="large" /> <Spin size="large" />
</div> </div>
......
import React from 'react'; import React from 'react';
import { formatMessage, setLocale, getLocale } from 'umi-plugin-locale'; import { formatMessage, setLocale, getLocale } from 'umi-plugin-locale';
import { Menu, Icon } from 'antd'; import { Menu, Icon } from 'antd';
import { ClickParam } from 'antd/es/menu';
import classNames from 'classnames'; import classNames from 'classnames';
import HeaderDropdown from '../HeaderDropdown'; import HeaderDropdown from '../HeaderDropdown';
import styles from './index.less'; import styles from './index.less';
...@@ -8,12 +9,10 @@ import styles from './index.less'; ...@@ -8,12 +9,10 @@ import styles from './index.less';
interface SelectLangProps { interface SelectLangProps {
className?: string; className?: string;
} }
const SelectLang: React.SFC<SelectLangProps> = props => { const SelectLang: React.FC<SelectLangProps> = props => {
const { className } = props; const { className } = props;
const selectedLang = getLocale(); const selectedLang = getLocale();
const changeLang = ({ key }) => { const changeLang = ({ key }: ClickParam) => setLocale(key);
setLocale(key);
};
const locales = ['zh-CN', 'zh-TW', 'en-US', 'pt-BR']; const locales = ['zh-CN', 'zh-TW', 'en-US', 'pt-BR'];
const languageLabels = { const languageLabels = {
'zh-CN': '简体中文', 'zh-CN': '简体中文',
......
...@@ -2,12 +2,13 @@ import React from 'react'; ...@@ -2,12 +2,13 @@ import React from 'react';
import { Tooltip, Icon } from 'antd'; import { Tooltip, Icon } from 'antd';
import style from './index.less'; import style from './index.less';
interface BlockChecboxProps { export interface BlockChecboxProps {
value: string; value: string;
onChange: (key: string) => void; onChange: (key: string) => void;
list: any[]; list: any[];
} }
const BlockChecbox: React.SFC<BlockChecboxProps> = ({ value, onChange, list }) => (
const BlockChecbox: React.FC<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}>
......
...@@ -3,68 +3,61 @@ import { Tooltip, Icon } from 'antd'; ...@@ -3,68 +3,61 @@ import { Tooltip, Icon } from 'antd';
import { formatMessage } from 'umi-plugin-locale'; import { formatMessage } from 'umi-plugin-locale';
import styles from './ThemeColor.less'; import styles from './ThemeColor.less';
interface TagProps { export interface TagProps {
color: string; color: string;
check: boolean; check: boolean;
className?: string; className?: string;
onClick?: () => void; onClick?: () => void;
} }
const Tag: React.SFC<TagProps> = ({ color, check, ...rest }) => (
<div const Tag: React.FC<TagProps> = ({ color, check, ...rest }) => (
{...rest} <div {...rest} style={{ backgroundColor: color }}>
style={{
backgroundColor: color,
}}
>
{check ? <Icon type="check" /> : ''} {check ? <Icon type="check" /> : ''}
</div> </div>
); );
interface ThemeColorProps { export interface ThemeColorProps {
colors?: any[]; colors?: any[];
title?: string; title?: string;
value: string; value: string;
onChange: (color: string) => void; onChange: (color: string) => void;
} }
const ThemeColor: React.SFC<ThemeColorProps> = ({ colors, title, value, onChange }) => { const ThemeColor: React.FC<ThemeColorProps> = ({ colors, title, value, onChange }) => {
let colorList = colors; const colorList = colors || [
if (!colors) { {
colorList = [ key: 'dust',
{ color: '#F5222D',
key: 'dust', },
color: '#F5222D', {
}, key: 'volcano',
{ color: '#FA541C',
key: 'volcano', },
color: '#FA541C', {
}, key: 'sunset',
{ color: '#FAAD14',
key: 'sunset', },
color: '#FAAD14', {
}, key: 'cyan',
{ color: '#13C2C2',
key: 'cyan', },
color: '#13C2C2', {
}, key: 'green',
{ color: '#52C41A',
key: 'green', },
color: '#52C41A', {
}, key: 'daybreak',
{ color: '#1890FF',
key: 'daybreak', },
color: '#1890FF', {
}, key: 'geekblue',
{ color: '#2F54EB',
key: 'geekblue', },
color: '#2F54EB', {
}, key: 'purple',
{ color: '#722ED1',
key: 'purple', },
color: '#722ED1', ];
},
];
}
return ( return (
<div className={styles.themeColor}> <div className={styles.themeColor}>
<h3 className={styles.title}>{title}</h3> <h3 className={styles.title}>{title}</h3>
......
import { ConnectProps, ConnectState, SettingModelState } from '@/models/connect';
import React, { Component } 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-plugin-locale'; import { formatMessage } from 'umi-plugin-locale';
...@@ -7,7 +8,6 @@ import omit from 'omit.js'; ...@@ -7,7 +8,6 @@ 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 { interface BodyProps {
...@@ -15,34 +15,37 @@ interface BodyProps { ...@@ -15,34 +15,37 @@ interface BodyProps {
style?: React.CSSProperties; style?: React.CSSProperties;
} }
const Body: React.SFC<BodyProps> = ({ children, title, style }) => ( const Body: React.FC<BodyProps> = ({ children, title, style }) => (
<div <div style={{ ...style, marginBottom: 24 }}>
style={{
...style,
marginBottom: 24,
}}
>
<h3 className={styles.title}>{title}</h3> <h3 className={styles.title}>{title}</h3>
{children} {children}
</div> </div>
); );
interface SettingDrawerProps { interface SettingItemProps {
setting?: DefaultSettings; title: React.ReactNode;
dispatch?: (args: any) => void; action: React.ReactElement;
disabled?: boolean;
disabledReason?: React.ReactNode;
} }
interface SettingDrawerState {}
@connect(({ setting }) => ({ setting })) export interface SettingDrawerProps extends ConnectProps {
setting?: SettingModelState;
}
export interface SettingDrawerState extends Partial<SettingModelState> {
collapse: boolean;
}
@connect(({ setting }: ConnectState) => ({ setting }))
class SettingDrawer extends Component<SettingDrawerProps, SettingDrawerState> { class SettingDrawer extends Component<SettingDrawerProps, SettingDrawerState> {
state = { state: SettingDrawerState = {
collapse: false, collapse: false,
}; };
getLayoutSetting = () => { getLayoutSetting = (): SettingItemProps[] => {
const { const { setting } = this.props;
setting: { contentWidth, fixedHeader, layout, autoHideHeader, fixSiderbar }, const { contentWidth, fixedHeader, layout, autoHideHeader, fixSiderbar } = setting!;
} = this.props;
return [ return [
{ {
title: formatMessage({ id: 'app.setting.content-width' }), title: formatMessage({ id: 'app.setting.content-width' }),
...@@ -101,9 +104,9 @@ class SettingDrawer extends Component<SettingDrawerProps, SettingDrawerState> { ...@@ -101,9 +104,9 @@ class SettingDrawer extends Component<SettingDrawerProps, SettingDrawerState> {
]; ];
}; };
changeSetting = (key, value) => { changeSetting = (key: string, value: any) => {
const { setting } = this.props; const { setting } = this.props;
const nextState = { ...setting }; const nextState = { ...setting! };
nextState[key] = value; nextState[key] = value;
if (key === 'layout') { if (key === 'layout') {
nextState.contentWidth = value === 'topmenu' ? 'Fixed' : 'Fluid'; nextState.contentWidth = value === 'topmenu' ? 'Fixed' : 'Fluid';
...@@ -112,7 +115,7 @@ class SettingDrawer extends Component<SettingDrawerProps, SettingDrawerState> { ...@@ -112,7 +115,7 @@ class SettingDrawer extends Component<SettingDrawerProps, SettingDrawerState> {
} }
this.setState(nextState, () => { this.setState(nextState, () => {
const { dispatch } = this.props; const { dispatch } = this.props;
dispatch({ dispatch!({
type: 'setting/changeSetting', type: 'setting/changeSetting',
payload: this.state, payload: this.state,
}); });
...@@ -124,7 +127,7 @@ class SettingDrawer extends Component<SettingDrawerProps, SettingDrawerState> { ...@@ -124,7 +127,7 @@ class SettingDrawer extends Component<SettingDrawerProps, SettingDrawerState> {
this.setState({ collapse: !collapse }); this.setState({ collapse: !collapse });
}; };
renderLayoutSettingItem = item => { renderLayoutSettingItem = (item: SettingItemProps) => {
const action = React.cloneElement(item.action, { const action = React.cloneElement(item.action, {
disabled: item.disabled, disabled: item.disabled,
}); });
...@@ -139,7 +142,7 @@ class SettingDrawer extends Component<SettingDrawerProps, SettingDrawerState> { ...@@ -139,7 +142,7 @@ class SettingDrawer extends Component<SettingDrawerProps, SettingDrawerState> {
render() { render() {
const { setting } = this.props; const { setting } = this.props;
const { navTheme, primaryColor, layout, colorWeak } = setting; const { navTheme, primaryColor, layout, colorWeak } = setting!;
const { collapse } = this.state; const { collapse } = this.state;
return ( return (
<Drawer <Drawer
...@@ -149,18 +152,10 @@ class SettingDrawer extends Component<SettingDrawerProps, SettingDrawerState> { ...@@ -149,18 +152,10 @@ class SettingDrawer extends Component<SettingDrawerProps, SettingDrawerState> {
placement="right" placement="right"
handler={ handler={
<div className={styles.handle} onClick={this.togglerContent}> <div className={styles.handle} onClick={this.togglerContent}>
<Icon <Icon type={collapse ? 'close' : 'setting'} style={{ color: '#fff', fontSize: 20 }} />
type={collapse ? 'close' : 'setting'}
style={{
color: '#fff',
fontSize: 20,
}}
/>
</div> </div>
} }
style={{ style={{ zIndex: 999 }}
zIndex: 999,
}}
> >
<div className={styles.content}> <div className={styles.content}>
<Body title={formatMessage({ id: 'app.setting.pagestyle' })}> <Body title={formatMessage({ id: 'app.setting.pagestyle' })}>
...@@ -221,6 +216,7 @@ class SettingDrawer extends Component<SettingDrawerProps, SettingDrawerState> { ...@@ -221,6 +216,7 @@ class SettingDrawer extends Component<SettingDrawerProps, SettingDrawerState> {
<List.Item <List.Item
actions={[ actions={[
<Switch <Switch
key="Switch"
size="small" size="small"
checked={!!colorWeak} checked={!!colorWeak}
onChange={checked => this.changeSetting('colorWeak', checked)} onChange={checked => this.changeSetting('colorWeak', checked)}
......
import IconFont from '@/components/IconFont'; import IconFont from '@/components/IconFont';
import { isUrl } from '@/utils/utils'; import { isUrl } from '@/utils/utils';
import { Icon, Menu } from 'antd'; import { Icon, Menu } from 'antd';
import { MenuMode, MenuTheme } from 'antd/es/menu';
import classNames from 'classnames'; import classNames from 'classnames';
import * as H from 'history';
import React, { Component } from 'react'; 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';
...@@ -16,7 +16,7 @@ const { SubMenu } = Menu; ...@@ -16,7 +16,7 @@ const { SubMenu } = Menu;
// icon: 'icon-geren' #For Iconfont , // icon: 'icon-geren' #For Iconfont ,
// icon: 'http://demo.com/icon.png', // icon: 'http://demo.com/icon.png',
// icon: <Icon type="setting" />, // icon: <Icon type="setting" />,
const getIcon = icon => { const getIcon = (icon?: string | React.ReactNode) => {
if (typeof icon === 'string') { if (typeof icon === 'string') {
if (isUrl(icon)) { if (isUrl(icon)) {
return <Icon component={() => <img src={icon} alt="icon" className={styles.icon} />} />; return <Icon component={() => <img src={icon} alt="icon" className={styles.icon} />} />;
...@@ -29,42 +29,55 @@ const getIcon = icon => { ...@@ -29,42 +29,55 @@ const getIcon = icon => {
return icon; return icon;
}; };
export declare type CollapseType = 'clickTrigger' | 'responsive'; /**
export declare type SiderTheme = 'light' | 'dark'; * @type R: is route
export declare type MenuMode = */
| 'vertical' export interface MenuDataItem<R extends boolean = false> {
| 'vertical-left' authority?: string[] | string;
| 'vertical-right' children?: MenuDataItem[];
| 'horizontal' hideChildrenInMenu?: boolean;
| 'inline'; hideInMenu?: boolean;
icon?: string;
locale?: string;
name?: string;
path: string;
routes?: R extends true ? MenuDataItem<R>[] : never;
[key: string]: any;
}
interface BaseMenuProps { export interface BaseMenuProps {
className?: string;
collapsed?: boolean;
flatMenuKeys?: any[]; flatMenuKeys?: any[];
location?: H.Location; handleOpenChange?: (openKeys: string[]) => void;
onCollapse?: (collapsed: boolean, type?: CollapseType) => void;
isMobile?: boolean; isMobile?: boolean;
openKeys?: any; location?: Location;
theme?: SiderTheme; menuData?: MenuDataItem[];
mode?: MenuMode; mode?: MenuMode;
className?: string; onCollapse?: (collapsed: boolean) => void;
collapsed?: boolean;
handleOpenChange?: (openKeys: any[]) => void;
menuData?: any[];
style?: React.CSSProperties;
onOpenChange?: (openKeys: string[]) => void; onOpenChange?: (openKeys: string[]) => void;
openKeys?: string[];
style?: React.CSSProperties;
theme?: MenuTheme;
} }
interface BaseMenuState {} export default class BaseMenu extends Component<BaseMenuProps> {
static defaultProps: BaseMenuProps = {
flatMenuKeys: [],
location: window.location,
onCollapse: () => void 0,
isMobile: false,
openKeys: [],
collapsed: false,
handleOpenChange: () => void 0,
menuData: [],
onOpenChange: () => void 0,
};
export default class BaseMenu extends Component<BaseMenuProps, BaseMenuState> {
/** /**
* 获得菜单子节点 * 获得菜单子节点
* @memberof SiderMenu
*/ */
getNavMenuItems: (menusData: any[]) => any[] = menusData => { getNavMenuItems = (menusData: MenuDataItem[] = []): React.ReactNode[] => {
if (!menusData) {
return [];
}
return menusData return menusData
.filter(item => item.name && !item.hideInMenu) .filter(item => item.name && !item.hideInMenu)
.map(item => this.getSubMenuOrItem(item)) .map(item => this.getSubMenuOrItem(item))
...@@ -72,18 +85,22 @@ export default class BaseMenu extends Component<BaseMenuProps, BaseMenuState> { ...@@ -72,18 +85,22 @@ export default class BaseMenu extends Component<BaseMenuProps, BaseMenuState> {
}; };
// Get the currently selected menu // Get the currently selected menu
getSelectedMenuKeys = pathname => { getSelectedMenuKeys = (pathname: string): string[] => {
const { flatMenuKeys } = this.props; const { flatMenuKeys } = this.props;
return urlToList(pathname).map(itemPath => getMenuMatches(flatMenuKeys, itemPath).pop()); return urlToList(pathname)
.map(itemPath => getMenuMatches(flatMenuKeys, itemPath).pop())
.filter(item => item) as string[];
}; };
/** /**
* get SubMenu or Item * get SubMenu or Item
*/ */
getSubMenuOrItem = item => { getSubMenuOrItem = (item: MenuDataItem): React.ReactNode => {
// doc: add hideChildrenInMenu if (
if (item.children && !item.hideChildrenInMenu && item.children.some(child => child.name)) { Array.isArray(item.children) &&
const { name } = item; !item.hideChildrenInMenu &&
item.children.some(child => (child.name ? true : false))
) {
return ( return (
<SubMenu <SubMenu
title={ title={
...@@ -93,7 +110,7 @@ export default class BaseMenu extends Component<BaseMenuProps, BaseMenuState> { ...@@ -93,7 +110,7 @@ export default class BaseMenu extends Component<BaseMenuProps, BaseMenuState> {
<span>{name}</span> <span>{name}</span>
</span> </span>
) : ( ) : (
name item.name
) )
} }
key={item.path} key={item.path}
...@@ -110,7 +127,7 @@ export default class BaseMenu extends Component<BaseMenuProps, BaseMenuState> { ...@@ -110,7 +127,7 @@ export default class BaseMenu extends Component<BaseMenuProps, BaseMenuState> {
* Judge whether it is http link.return a or Link * Judge whether it is http link.return a or Link
* @memberof SiderMenu * @memberof SiderMenu
*/ */
getMenuItemPath = item => { getMenuItemPath = (item: MenuDataItem) => {
const { name } = item; const { name } = item;
const itemPath = this.conversionPath(item.path); const itemPath = this.conversionPath(item.path);
const icon = getIcon(item.icon); const icon = getIcon(item.icon);
...@@ -129,14 +146,8 @@ export default class BaseMenu extends Component<BaseMenuProps, BaseMenuState> { ...@@ -129,14 +146,8 @@ export default class BaseMenu extends Component<BaseMenuProps, BaseMenuState> {
<Link <Link
to={itemPath} to={itemPath}
target={target} target={target}
replace={itemPath === location.pathname} replace={itemPath === location!.pathname}
onClick={ onClick={isMobile ? () => onCollapse!(true) : void 0}
isMobile
? () => {
onCollapse(true);
}
: undefined
}
> >
{icon} {icon}
<span>{name}</span> <span>{name}</span>
...@@ -144,7 +155,7 @@ export default class BaseMenu extends Component<BaseMenuProps, BaseMenuState> { ...@@ -144,7 +155,7 @@ export default class BaseMenu extends Component<BaseMenuProps, BaseMenuState> {
); );
}; };
conversionPath = path => { conversionPath = (path: string) => {
if (path && path.indexOf('http') === 0) { if (path && path.indexOf('http') === 0) {
return path; return path;
} }
...@@ -156,7 +167,7 @@ export default class BaseMenu extends Component<BaseMenuProps, BaseMenuState> { ...@@ -156,7 +167,7 @@ export default class BaseMenu extends Component<BaseMenuProps, BaseMenuState> {
openKeys, openKeys,
theme, theme,
mode, mode,
location: { pathname }, location,
className, className,
collapsed, collapsed,
handleOpenChange, handleOpenChange,
...@@ -164,7 +175,7 @@ export default class BaseMenu extends Component<BaseMenuProps, BaseMenuState> { ...@@ -164,7 +175,7 @@ export default class BaseMenu extends Component<BaseMenuProps, BaseMenuState> {
menuData, 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(location!.pathname);
if (!selectedKeys.length && openKeys) { if (!selectedKeys.length && openKeys) {
selectedKeys = [openKeys[openKeys.length - 1]]; selectedKeys = [openKeys[openKeys.length - 1]];
} }
......
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 React, { Component, Suspense } from 'react';
import Link from 'umi/link'; import Link from 'umi/link';
import defaultSettings from '../../../config/defaultSettings'; import defaultSettings from '../../../config/defaultSettings';
import PageLoading from '../PageLoading'; import PageLoading from '../PageLoading';
import { BaseMenuProps } from './BaseMenu';
import styles from './index.less'; import styles from './index.less';
import { getDefaultCollapsedSubMenus } from './SiderMenuUtils'; import { getDefaultCollapsedSubMenus } from './SiderMenuUtils';
...@@ -13,38 +13,42 @@ const { Sider } = Layout; ...@@ -13,38 +13,42 @@ const { Sider } = Layout;
const { title } = defaultSettings; const { title } = defaultSettings;
let firstMount: boolean = true; let firstMount: boolean = true;
export declare type CollapseType = 'clickTrigger' | 'responsive'; export interface SiderMenuProps extends BaseMenuProps {
export declare type SiderTheme = 'light' | 'dark';
interface SiderMenuProps {
menuData: any[];
location?: H.Location;
flatMenuKeys?: any[];
logo?: string; logo?: string;
collapsed: boolean;
onCollapse: (collapsed: boolean, type?: CollapseType) => void;
fixSiderbar?: boolean; fixSiderbar?: boolean;
theme?: SiderTheme;
isMobile: boolean;
} }
interface SiderMenuState { interface SiderMenuState {
openKeys: any; pathname?: string;
openKeys?: string[];
flatMenuKeysLen?: number; flatMenuKeysLen?: number;
} }
export default class SiderMenu extends Component<SiderMenuProps, SiderMenuState> { export default class SiderMenu extends Component<SiderMenuProps, SiderMenuState> {
static getDerivedStateFromProps(props, state) { static defaultProps: SiderMenuProps = {
flatMenuKeys: [],
location: window.location,
onCollapse: () => void 0,
isMobile: false,
openKeys: [],
collapsed: false,
handleOpenChange: () => void 0,
menuData: [],
onOpenChange: () => void 0,
};
static getDerivedStateFromProps(props: SiderMenuProps, state: SiderMenuState) {
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) {
return { return {
pathname: props.location.pathname, pathname: props.location!.pathname,
flatMenuKeysLen: props.flatMenuKeys.length, flatMenuKeysLen: props.flatMenuKeys!.length,
openKeys: getDefaultCollapsedSubMenus(props), openKeys: getDefaultCollapsedSubMenus(props),
}; };
} }
return null; return null;
} }
constructor(props: SiderMenuProps) { constructor(props: SiderMenuProps) {
super(props); super(props);
this.state = { this.state = {
...@@ -58,7 +62,7 @@ export default class SiderMenu extends Component<SiderMenuProps, SiderMenuState> ...@@ -58,7 +62,7 @@ export default class SiderMenu extends Component<SiderMenuProps, SiderMenuState>
isMainMenu: (key: string) => boolean = 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) {
return item.key === key || item.path === key; return item.key === key || item.path === key;
} }
...@@ -66,11 +70,13 @@ export default class SiderMenu extends Component<SiderMenuProps, SiderMenuState> ...@@ -66,11 +70,13 @@ export default class SiderMenu extends Component<SiderMenuProps, SiderMenuState>
}); });
}; };
handleOpenChange: (openKeys: any[]) => void = openKeys => { handleOpenChange: (openKeys: string[]) => void = openKeys => {
const moreThanOne = openKeys.filter(openKey => this.isMainMenu(openKey)).length > 1; const moreThanOne = openKeys.filter(openKey => this.isMainMenu(openKey)).length > 1;
this.setState({ if (moreThanOne) {
openKeys: moreThanOne ? [openKeys.pop()] : [...openKeys], this.setState({ openKeys: [openKeys.pop()].filter(item => item) as string[] });
}); } else {
this.setState({ openKeys: [...openKeys] });
}
}; };
render() { render() {
...@@ -84,13 +90,13 @@ export default class SiderMenu extends Component<SiderMenuProps, SiderMenuState> ...@@ -84,13 +90,13 @@ export default class SiderMenu extends Component<SiderMenuProps, SiderMenuState>
}); });
return ( return (
<Sider <Sider
collapsible
trigger={null} trigger={null}
collapsible={true}
collapsed={collapsed} collapsed={collapsed}
breakpoint="lg" breakpoint="lg"
onCollapse={collapse => { onCollapse={collapse => {
if (firstMount || !isMobile) { if (firstMount || !isMobile) {
onCollapse(collapse); onCollapse!(collapse);
} }
}} }}
width={256} width={256}
......
import pathToRegexp from 'path-to-regexp'; import pathToRegexp from 'path-to-regexp';
import { urlToList } from '../_utils/pathTools'; import { urlToList } from '../_utils/pathTools';
import { MenuDataItem, BaseMenuProps } from './BaseMenu';
/** /**
* Recursively flatten the data * Recursively flatten the data
* [{path:string},{path:string}] => {path,path2} * [{path:string},{path:string}] => {path,path2}
* @param menus * @param menus
*/ */
export const getFlatMenuKeys = menuData => { export const getFlatMenuKeys = (menuData: MenuDataItem[] = []) => {
let keys = []; let keys: string[] = [];
menuData.forEach(item => { menuData.forEach(item => {
keys.push(item.path); keys.push(item.path);
if (item.children) { if (item.children) {
...@@ -17,24 +18,15 @@ export const getFlatMenuKeys = menuData => { ...@@ -17,24 +18,15 @@ export const getFlatMenuKeys = menuData => {
return keys; return keys;
}; };
export const getMenuMatches = (flatMenuKeys, path) => export const getMenuMatches = (flatMenuKeys: string[] = [], path: string) =>
flatMenuKeys.filter(item => { flatMenuKeys.filter(item => item && pathToRegexp(item).test(path));
if (item) {
return pathToRegexp(item).test(path);
}
return false;
});
/** /**
* 获得菜单子节点 * 获得菜单子节点
* @memberof SiderMenu
*/ */
export const getDefaultCollapsedSubMenus = props => { export const getDefaultCollapsedSubMenus = (props: BaseMenuProps) => {
const { const { location, flatMenuKeys } = props;
location: { pathname }, return urlToList(location!.pathname)
flatMenuKeys,
} = props;
return urlToList(pathname)
.map(item => getMenuMatches(flatMenuKeys, item)[0]) .map(item => getMenuMatches(flatMenuKeys, item)[0])
.filter(item => item) .filter(item => item)
.reduce((acc, curr) => [...acc, curr], ['/']); .reduce((acc, curr) => [...acc, curr], ['/']);
......
import React from 'react'; import React from 'react';
import { Drawer } from 'antd'; import { Drawer } from 'antd';
import { SiderMenuProps } from './SiderMenu';
import SiderMenu from './SiderMenu'; import SiderMenu from './SiderMenu';
import { getFlatMenuKeys } from './SiderMenuUtils'; import { getFlatMenuKeys } from './SiderMenuUtils';
export declare type SiderTheme = 'light' | 'dark'; export { SiderMenuProps };
export { MenuDataItem } from './BaseMenu';
interface SiderMenuProps { const SiderMenuWrapper: React.FC<SiderMenuProps> = props => {
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 ? (
<Drawer <Drawer
visible={!collapsed} visible={!collapsed}
placement="left" placement="left"
onClose={() => onCollapse(true)} onClose={() => onCollapse!(true)}
style={{ style={{
padding: 0, padding: 0,
height: '100vh', height: '100vh',
...@@ -34,4 +27,8 @@ const SiderMenuWrapper: React.SFC<SiderMenuProps> = props => { ...@@ -34,4 +27,8 @@ const SiderMenuWrapper: React.SFC<SiderMenuProps> = props => {
); );
}; };
SiderMenuWrapper.defaultProps = {
onCollapse: () => void 0,
};
export default React.memo(SiderMenuWrapper); export default React.memo(SiderMenuWrapper);
import { SiderMenuProps } from '@/components/SiderMenu';
import React, { Component } from 'react'; import React, { Component } from 'react';
import Link from 'umi/link'; import Link from 'umi/link';
import RightContent from '../GlobalHeader/RightContent'; import RightContent, { GlobalHeaderRightProps } 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 defaultSettings from '../../../config/defaultSettings'; import defaultSettings, { ContentWidth } from '../../../config/defaultSettings';
export declare type CollapseType = 'clickTrigger' | 'responsive'; export interface TopNavHeaderProps extends SiderMenuProps, GlobalHeaderRightProps {
export declare type SiderTheme = 'light' | 'dark'; contentWidth?: ContentWidth;
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 { interface TopNavHeaderState {
maxWidth: undefined | number; maxWidth?: number;
} }
export default class TopNavHeader extends Component<TopNavHeaderProps, TopNavHeaderState> { export default class TopNavHeader extends Component<TopNavHeaderProps, TopNavHeaderState> {
state = { static getDerivedStateFromProps(props: TopNavHeaderProps) {
maxWidth: undefined,
};
maim: HTMLDivElement;
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,
}; };
} }
state: TopNavHeaderState = {};
maim: HTMLDivElement | null = null;
render() { render() {
const { theme, contentWidth, menuData, logo } = this.props; const { theme, contentWidth, menuData, logo } = this.props;
const { maxWidth } = this.state; const { maxWidth } = this.state;
...@@ -60,23 +33,17 @@ export default class TopNavHeader extends Component<TopNavHeaderProps, TopNavHea ...@@ -60,23 +33,17 @@ export default class TopNavHeader extends Component<TopNavHeaderProps, TopNavHea
return ( return (
<div className={`${styles.head} ${theme === 'light' ? styles.light : ''}`}> <div className={`${styles.head} ${theme === 'light' ? styles.light : ''}`}>
<div <div
ref={ref => { ref={ref => (this.maim = ref)}
this.maim = ref;
}}
className={`${styles.main} ${contentWidth === 'Fixed' ? styles.wide : ''}`} className={`${styles.main} ${contentWidth === 'Fixed' ? styles.wide : ''}`}
> >
<div className={styles.left}> <div className={styles.left}>
<div className={styles.logo} key="logo" id="logo"> <div className={styles.logo} key="logo" id="logo">
<Link to="/"> <Link to="/">
<img src={logo} alt="logo" /> <img src={logo} alt="logo" />
<h1>{title}</h1> <h1>{defaultSettings.title}</h1>
</Link> </Link>
</div> </div>
<div <div style={{ maxWidth }}>
style={{
maxWidth,
}}
>
<BaseMenu {...this.props} flatMenuKeys={flatMenuKeys} className={styles.menu} /> <BaseMenu {...this.props} flatMenuKeys={flatMenuKeys} className={styles.menu} />
</div> </div>
</div> </div>
......
import 'jest';
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',
]);
});
});
// /userinfo/2144/id => ['/userinfo','/useinfo/2144,'/userindo/2144/id'] // /userinfo/2144/id => ['/userinfo','/useinfo/2144,'/userindo/2144/id']
// eslint-disable-next-line import/prefer-default-export // eslint-disable-next-line import/prefer-default-export
export function urlToList(url) { export function urlToList(url: string) {
const urllist = url.split('/').filter(i => i); const urllist = url.split('/').filter(i => i);
return urllist.map((urlItem, index) => `/${urllist.slice(0, index + 1).join('/')}`); return urllist.map((urlItem, index) => `/${urllist.slice(0, index + 1).join('/')}`);
} }
...@@ -14,7 +14,8 @@ if (pwa) { ...@@ -14,7 +14,8 @@ 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: CustomEvent) => { window.addEventListener('sw.updated', (event: Event) => {
const e = event as 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
...@@ -25,11 +26,11 @@ if (pwa) { ...@@ -25,11 +26,11 @@ if (pwa) {
// Send skip-waiting event to waiting SW with MessageChannel // Send skip-waiting event to waiting SW with MessageChannel
await new Promise((resolve, reject) => { await new Promise((resolve, reject) => {
const channel = new MessageChannel(); const channel = new MessageChannel();
channel.port1.onmessage = event => { channel.port1.onmessage = msgEvent => {
if (event.data.error) { if (msgEvent.data.error) {
reject(event.data.error); reject(msgEvent.data.error);
} else { } else {
resolve(event.data); resolve(msgEvent.data);
} }
}; };
worker.postMessage({ type: 'skip-waiting' }, [channel.port2]); worker.postMessage({ type: 'skip-waiting' }, [channel.port2]);
......
import PageLoading from '@/components/PageLoading'; import PageLoading from '@/components/PageLoading';
import SiderMenu from '@/components/SiderMenu'; import SiderMenu, { MenuDataItem, SiderMenuProps } from '@/components/SiderMenu';
import { ConnectProps, ConnectState, SettingModelState } from '@/models/connect';
import getPageTitle from '@/utils/getPageTitle'; import getPageTitle from '@/utils/getPageTitle';
import { Layout } from 'antd'; import { Layout } from 'antd';
import classNames from 'classnames'; import classNames from 'classnames';
...@@ -11,7 +12,7 @@ import useMedia from 'react-media-hook2'; ...@@ -11,7 +12,7 @@ import useMedia from 'react-media-hook2';
import logo from '../assets/logo.svg'; import logo from '../assets/logo.svg';
import styles from './BasicLayout.less'; import styles from './BasicLayout.less';
import Footer from './Footer'; import Footer from './Footer';
import Header from './Header'; import Header, { HeaderViewProps } from './Header';
import Context from './MenuContext'; import Context from './MenuContext';
// lazy load SettingDrawer // lazy load SettingDrawer
...@@ -44,28 +45,21 @@ const query = { ...@@ -44,28 +45,21 @@ const query = {
}, },
}; };
export declare type SiderTheme = 'light' | 'dark'; export interface BasicLayoutProps
extends ConnectProps,
interface BasicLayoutProps { SiderMenuProps,
dispatch: (args: any) => void; HeaderViewProps,
// wait for https://github.com/umijs/umi/pull/2036 Partial<SettingModelState> {
route: any; breadcrumbNameMap: { [path: string]: MenuDataItem };
breadcrumbNameMap: object; route: MenuDataItem;
fixSiderbar: boolean;
layout: string;
navTheme: SiderTheme;
menuData: any[];
fixedHeader: boolean;
location: Location;
collapsed: boolean;
} }
interface BasicLayoutContext { export interface BasicLayoutContext {
location: Location; location: Location;
breadcrumbNameMap: object; breadcrumbNameMap: { [path: string]: MenuDataItem };
} }
const BasicLayout: React.SFC<BasicLayoutProps> = props => { const BasicLayout: React.FC<BasicLayoutProps> = props => {
const { const {
breadcrumbNameMap, breadcrumbNameMap,
dispatch, dispatch,
...@@ -79,18 +73,21 @@ const BasicLayout: React.SFC<BasicLayoutProps> = props => { ...@@ -79,18 +73,21 @@ const BasicLayout: React.SFC<BasicLayoutProps> = props => {
navTheme, navTheme,
route: { routes, authority }, route: { routes, authority },
} = props; } = props;
/**
* constructor
*/
useState(() => { useState(() => {
dispatch({ type: 'user/fetchCurrent' }); dispatch!({ type: 'user/fetchCurrent' });
dispatch({ type: 'setting/getSetting' }); dispatch!({ type: 'setting/getSetting' });
dispatch({ type: 'menu/getMenuData', payload: { routes, authority } }); dispatch!({ type: 'menu/getMenuData', payload: { routes, authority } });
}); });
const isTop = PropsLayout === 'topmenu'; /**
const contentStyle = !fixedHeader ? { paddingTop: 0 } : {}; * init variables
*/
const isMobile = useMedia({ id: 'BasicLayout', query: '(max-width: 599px)' })[0]; const isMobile = useMedia({ id: 'BasicLayout', query: '(max-width: 599px)' })[0];
const hasLeftPadding = fixSiderbar && PropsLayout !== 'topmenu' && !isMobile; const hasLeftPadding = fixSiderbar && PropsLayout !== 'topmenu' && !isMobile;
const getContext = (): BasicLayoutContext => ({ location, breadcrumbNameMap });
const handleMenuCollapse = (payload: boolean) => const handleMenuCollapse = (payload: boolean) =>
dispatch({ type: 'global/changeLayoutCollapsed', payload }); dispatch!({ type: 'global/changeLayoutCollapsed', payload });
// Do not render SettingDrawer in production // Do not render SettingDrawer in production
// unless it is deployed in preview.pro.ant.design as demo // unless it is deployed in preview.pro.ant.design as demo
const renderSettingDrawer = () => const renderSettingDrawer = () =>
...@@ -98,7 +95,7 @@ const BasicLayout: React.SFC<BasicLayoutProps> = props => { ...@@ -98,7 +95,7 @@ const BasicLayout: React.SFC<BasicLayoutProps> = props => {
const layout = ( const layout = (
<Layout> <Layout>
{isTop && !isMobile ? null : ( {PropsLayout === 'topmenu' && !isMobile ? null : (
<SiderMenu <SiderMenu
logo={logo} logo={logo}
theme={navTheme} theme={navTheme}
...@@ -121,7 +118,7 @@ const BasicLayout: React.SFC<BasicLayoutProps> = props => { ...@@ -121,7 +118,7 @@ const BasicLayout: React.SFC<BasicLayoutProps> = props => {
isMobile={isMobile} isMobile={isMobile}
{...props} {...props}
/> />
<Content className={styles.content} style={contentStyle}> <Content className={styles.content} style={!fixedHeader ? { paddingTop: 0 } : {}}>
{children} {children}
</Content> </Content>
<Footer /> <Footer />
...@@ -130,10 +127,10 @@ const BasicLayout: React.SFC<BasicLayoutProps> = props => { ...@@ -130,10 +127,10 @@ const BasicLayout: React.SFC<BasicLayoutProps> = props => {
); );
return ( return (
<React.Fragment> <React.Fragment>
<DocumentTitle title={getPageTitle(location.pathname, breadcrumbNameMap)}> <DocumentTitle title={getPageTitle(location!.pathname, breadcrumbNameMap)}>
<ContainerQuery query={query}> <ContainerQuery query={query}>
{params => ( {params => (
<Context.Provider value={getContext()}> <Context.Provider value={{ location: location!, breadcrumbNameMap }}>
<div className={classNames(params)}>{layout}</div> <div className={classNames(params)}>{layout}</div>
</Context.Provider> </Context.Provider>
)} )}
...@@ -144,7 +141,7 @@ const BasicLayout: React.SFC<BasicLayoutProps> = props => { ...@@ -144,7 +141,7 @@ const BasicLayout: React.SFC<BasicLayoutProps> = props => {
); );
}; };
export default connect(({ global, setting, menu: menuModel }) => ({ export default connect(({ global, setting, menu: menuModel }: ConnectState) => ({
collapsed: global.collapsed, collapsed: global.collapsed,
layout: setting.layout, layout: setting.layout,
menuData: menuModel.menuData, menuData: menuModel.menuData,
......
import React, { ReactNode, SFC } from 'react'; import React from 'react';
interface BlankLayoutProps { const Layout: React.FC = ({ children }) => <div>{children}</div>;
children: ReactNode;
}
const Layout: SFC<BlankLayoutProps> = ({ children }) => <div>{children}</div>;
export default Layout; export default Layout;
import GlobalHeader from '@/components/GlobalHeader'; import GlobalHeader, { GlobalHeaderProps } from '@/components/GlobalHeader';
import TopNavHeader from '@/components/TopNavHeader'; import TopNavHeader, { TopNavHeaderProps } from '@/components/TopNavHeader';
import { DefaultSettings } from '../../config/defaultSettings'; import { ConnectProps, ConnectState, SettingModelState } from '@/models/connect';
import { Layout, message } from 'antd'; import { Layout, message } from 'antd';
import { ClickParam } from 'antd/es/menu';
import { connect } from 'dva'; import { connect } from 'dva';
import Animate from 'rc-animate'; import Animate from 'rc-animate';
import React, { Component } from 'react'; import React, { Component } from 'react';
...@@ -11,15 +12,12 @@ import styles from './Header.less'; ...@@ -11,15 +12,12 @@ import styles from './Header.less';
const { Header } = Layout; const { Header } = Layout;
export declare type SiderTheme = 'light' | 'dark'; export interface HeaderViewProps extends ConnectProps, TopNavHeaderProps, GlobalHeaderProps {
isMobile?: boolean;
interface HeaderViewProps { collapsed?: boolean;
isMobile: boolean; setting?: SettingModelState;
collapsed: boolean; autoHideHeader?: boolean;
setting: DefaultSettings; handleMenuCollapse?: (collapse: boolean) => void;
dispatch: (args: any) => void;
autoHideHeader: boolean;
handleMenuCollapse: (args: boolean) => void;
} }
interface HeaderViewState { interface HeaderViewState {
...@@ -27,6 +25,10 @@ interface HeaderViewState { ...@@ -27,6 +25,10 @@ interface HeaderViewState {
} }
class HeaderView extends Component<HeaderViewProps, HeaderViewState> { class HeaderView extends Component<HeaderViewProps, HeaderViewState> {
static defaultProps: HeaderViewProps = {
handleMenuCollapse: () => void 0,
};
static getDerivedStateFromProps(props: HeaderViewProps, state: HeaderViewState) { static getDerivedStateFromProps(props: HeaderViewProps, state: HeaderViewState) {
if (!props.autoHideHeader && !state.visible) { if (!props.autoHideHeader && !state.visible) {
return { return {
...@@ -36,14 +38,12 @@ class HeaderView extends Component<HeaderViewProps, HeaderViewState> { ...@@ -36,14 +38,12 @@ class HeaderView extends Component<HeaderViewProps, HeaderViewState> {
return null; return null;
} }
state = { ticking: boolean = false;
oldScrollTop: number = 0;
state: HeaderViewState = {
visible: true, visible: true,
}; };
ticking: boolean;
oldScrollTop: number;
componentDidMount() { componentDidMount() {
document.addEventListener('scroll', this.handScroll, { passive: true }); document.addEventListener('scroll', this.handScroll, { passive: true });
} }
...@@ -54,27 +54,27 @@ class HeaderView extends Component<HeaderViewProps, HeaderViewState> { ...@@ -54,27 +54,27 @@ class HeaderView extends Component<HeaderViewProps, HeaderViewState> {
getHeadWidth = () => { getHeadWidth = () => {
const { isMobile, collapsed, setting } = this.props; const { isMobile, collapsed, setting } = this.props;
const { fixedHeader, layout } = setting; const { fixedHeader, layout } = setting!;
if (isMobile || !fixedHeader || layout === 'topmenu') { if (isMobile || !fixedHeader || layout === 'topmenu') {
return '100%'; return '100%';
} }
return collapsed ? 'calc(100% - 80px)' : 'calc(100% - 256px)'; return collapsed ? 'calc(100% - 80px)' : 'calc(100% - 256px)';
}; };
handleNoticeClear = type => { handleNoticeClear = (type: string) => {
const { dispatch } = this.props;
message.success( message.success(
`${formatMessage({ id: 'component.noticeIcon.cleared' })} ${formatMessage({ `${formatMessage({ id: 'component.noticeIcon.cleared' })} ${formatMessage({
id: `component.globalHeader.${type}`, id: `component.globalHeader.${type}`,
})}` })}`,
); );
const { dispatch } = this.props; dispatch!({
dispatch({
type: 'global/clearNotices', type: 'global/clearNotices',
payload: type, payload: type,
}); });
}; };
handleMenuClick = ({ key }) => { handleMenuClick = ({ key }: ClickParam) => {
const { dispatch } = this.props; const { dispatch } = this.props;
if (key === 'userCenter') { if (key === 'userCenter') {
router.push('/account/center'); router.push('/account/center');
...@@ -89,16 +89,16 @@ class HeaderView extends Component<HeaderViewProps, HeaderViewState> { ...@@ -89,16 +89,16 @@ class HeaderView extends Component<HeaderViewProps, HeaderViewState> {
return; return;
} }
if (key === 'logout') { if (key === 'logout') {
dispatch({ dispatch!({
type: 'login/logout', type: 'login/logout',
}); });
} }
}; };
handleNoticeVisibleChange = visible => { handleNoticeVisibleChange = (visible: boolean) => {
if (visible) { if (visible) {
const { dispatch } = this.props; const { dispatch } = this.props;
dispatch({ dispatch!({
type: 'global/fetchNotices', type: 'global/fetchNotices',
}); });
} }
...@@ -135,7 +135,7 @@ class HeaderView extends Component<HeaderViewProps, HeaderViewState> { ...@@ -135,7 +135,7 @@ class HeaderView extends Component<HeaderViewProps, HeaderViewState> {
render() { render() {
const { isMobile, handleMenuCollapse, setting } = this.props; const { isMobile, handleMenuCollapse, setting } = this.props;
const { navTheme, layout, fixedHeader } = setting; const { navTheme, layout, fixedHeader } = setting!;
const { visible } = this.state; const { visible } = this.state;
const isTop = layout === 'topmenu'; const isTop = layout === 'topmenu';
const width = this.getHeadWidth(); const width = this.getHeadWidth();
...@@ -143,7 +143,7 @@ class HeaderView extends Component<HeaderViewProps, HeaderViewState> { ...@@ -143,7 +143,7 @@ class HeaderView extends Component<HeaderViewProps, HeaderViewState> {
<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 as SiderTheme} theme={navTheme}
mode="horizontal" mode="horizontal"
onCollapse={handleMenuCollapse} onCollapse={handleMenuCollapse}
onNoticeClear={this.handleNoticeClear} onNoticeClear={this.handleNoticeClear}
...@@ -170,7 +170,7 @@ class HeaderView extends Component<HeaderViewProps, HeaderViewState> { ...@@ -170,7 +170,7 @@ class HeaderView extends Component<HeaderViewProps, HeaderViewState> {
} }
} }
export default connect(({ user, global, setting, loading }) => ({ export default connect(({ user, global, setting, loading }: ConnectState) => ({
currentUser: user.currentUser, currentUser: user.currentUser,
collapsed: global.collapsed, collapsed: global.collapsed,
fetchingMoreNotices: loading.effects['global/fetchMoreNotices'], fetchingMoreNotices: loading.effects['global/fetchMoreNotices'],
......
import SelectLang from '@/components/SelectLang'; import SelectLang from '@/components/SelectLang';
import { MenuDataItem } from '@/components/SiderMenu';
import { ConnectProps, ConnectState } from '@/models/connect';
import getPageTitle from '@/utils/getPageTitle'; import getPageTitle from '@/utils/getPageTitle';
import { GlobalFooter } from 'ant-design-pro'; import { GlobalFooter } from 'ant-design-pro';
import { Icon } from 'antd'; import { Icon } from 'antd';
...@@ -34,12 +36,10 @@ const copyright = ( ...@@ -34,12 +36,10 @@ const copyright = (
</Fragment> </Fragment>
); );
interface UserLayoutProps { export interface UserLayoutProps extends ConnectProps {
dispatch: (args: any) => void; route: MenuDataItem;
route: any; breadcrumbNameMap: { [path: string]: MenuDataItem };
breadcrumbNameMap: object;
navTheme: string; navTheme: string;
location: Location;
} }
class UserLayout extends Component<UserLayoutProps> { class UserLayout extends Component<UserLayoutProps> {
...@@ -48,20 +48,16 @@ class UserLayout extends Component<UserLayoutProps> { ...@@ -48,20 +48,16 @@ class UserLayout extends Component<UserLayoutProps> {
dispatch, dispatch,
route: { routes, authority }, route: { routes, authority },
} = this.props; } = this.props;
dispatch({ dispatch!({
type: 'menu/getMenuData', type: 'menu/getMenuData',
payload: { routes, authority }, payload: { routes, authority },
}); });
} }
render() { render() {
const { const { children, location, breadcrumbNameMap } = this.props;
children,
location: { pathname },
breadcrumbNameMap,
} = this.props;
return ( return (
<DocumentTitle title={getPageTitle(pathname, breadcrumbNameMap)}> <DocumentTitle title={getPageTitle(location!.pathname, breadcrumbNameMap)}>
<div className={styles.container}> <div className={styles.container}>
<div className={styles.lang}> <div className={styles.lang}>
<SelectLang /> <SelectLang />
...@@ -85,7 +81,7 @@ class UserLayout extends Component<UserLayoutProps> { ...@@ -85,7 +81,7 @@ class UserLayout extends Component<UserLayoutProps> {
} }
} }
export default connect(({ menu: menuModel }) => ({ export default connect(({ menu: menuModel }: ConnectState) => ({
menuData: menuModel.menuData, menuData: menuModel.menuData,
breadcrumbNameMap: menuModel.breadcrumbNameMap, breadcrumbNameMap: menuModel.breadcrumbNameMap,
}))(UserLayout); }))(UserLayout);
import { EffectsCommandMap } from 'dva';
import { AnyAction } from 'redux';
import { GlobalModelState } from './global';
import { MenuModelState } from './menu';
import { UserModelState } from './user';
import { DefaultSettings as SettingModelState } from '../../config/defaultSettings';
export { GlobalModelState, MenuModelState, SettingModelState, UserModelState };
export type Effect = (
action: AnyAction,
effects: EffectsCommandMap & { select: <T>(func: (state: ConnectState) => T) => T },
) => void;
/**
* @type P: Type of payload
* @type C: Type of callback
*/
export type Dispatch = <P = any, C = (payload: P) => void>(action: {
type: string;
payload?: P;
callback?: C;
[key: string]: any;
}) => any;
export interface Loading {
global: boolean;
effects: { [key: string]: boolean | undefined };
models: {
global?: boolean;
menu?: boolean;
setting?: boolean;
user?: boolean;
};
}
export interface ConnectState {
global: GlobalModelState;
loading: Loading;
menu: MenuModelState;
setting: SettingModelState;
user: UserModelState;
}
/**
* @type T: Params matched in dynamic routing
* @type R: Instance type of ref
*/
export interface ConnectProps<T extends { [key: string]: any } = {}, R = any>
extends React.Props<R> {
dispatch?: Dispatch;
location?: Location;
match?: {
isExact: boolean;
params: T;
path: string;
url: string;
};
}
export default ConnectState;
import { queryNotices } from '@/services/user'; import { queryNotices } from '@/services/user';
import { Effect, Subscription } from 'dva'; import { Subscription } from 'dva';
import { Reducer } from 'redux'; import { Reducer } from 'redux';
import { Effect } from './connect';
import { INoticeIconData } from 'ant-design-pro/lib/NoticeIcon/NoticeIconTab';
export interface NoticeItem extends INoticeIconData {
id: string;
type: string;
[key: string]: any;
}
export interface GlobalModelState { export interface GlobalModelState {
collapsed: boolean; collapsed: boolean;
notices: any[]; notices: NoticeItem[];
} }
export interface GlobalModelType { export interface GlobalModelType {
...@@ -16,9 +24,9 @@ export interface GlobalModelType { ...@@ -16,9 +24,9 @@ export interface GlobalModelType {
changeNoticeReadState: Effect; changeNoticeReadState: Effect;
}; };
reducers: { reducers: {
changeLayoutCollapsed: Reducer<any>; changeLayoutCollapsed: Reducer<GlobalModelState>;
saveNotices: Reducer<any>; saveNotices: Reducer<GlobalModelState>;
saveClearedNotices: Reducer<any>; saveClearedNotices: Reducer<GlobalModelState>;
}; };
subscriptions: { setup: Subscription }; subscriptions: { setup: Subscription };
} }
...@@ -38,8 +46,8 @@ const GlobalModel: GlobalModelType = { ...@@ -38,8 +46,8 @@ const GlobalModel: GlobalModelType = {
type: 'saveNotices', type: 'saveNotices',
payload: data, payload: data,
}); });
const unreadCount = yield select( const unreadCount: number = yield select(
state => state.global.notices.filter(item => !item.read).length state => state.global.notices.filter(item => !item.read).length,
); );
yield put({ yield put({
type: 'user/changeNotifyCount', type: 'user/changeNotifyCount',
...@@ -54,9 +62,9 @@ const GlobalModel: GlobalModelType = { ...@@ -54,9 +62,9 @@ const GlobalModel: GlobalModelType = {
type: 'saveClearedNotices', type: 'saveClearedNotices',
payload, payload,
}); });
const count = yield select(state => state.global.notices.length); const count: number = yield select(state => state.global.notices.length);
const unreadCount = yield select( const unreadCount: number = yield select(
state => state.global.notices.filter(item => !item.read).length state => state.global.notices.filter(item => !item.read).length,
); );
yield put({ yield put({
type: 'user/changeNotifyCount', type: 'user/changeNotifyCount',
...@@ -67,14 +75,14 @@ const GlobalModel: GlobalModelType = { ...@@ -67,14 +75,14 @@ const GlobalModel: GlobalModelType = {
}); });
}, },
*changeNoticeReadState({ payload }, { put, select }) { *changeNoticeReadState({ payload }, { put, select }) {
const notices = yield select(state => const notices: NoticeItem[] = yield select(state =>
state.global.notices.map(item => { state.global.notices.map(item => {
const notice = { ...item }; const notice = { ...item };
if (notice.id === payload) { if (notice.id === payload) {
notice.read = true; notice.read = true;
} }
return notice; return notice;
}) }),
); );
yield put({ yield put({
type: 'saveNotices', type: 'saveNotices',
......
import { MenuDataItem } from '@/components/SiderMenu';
import Authorized from '@/utils/Authorized'; import Authorized from '@/utils/Authorized';
import { Effect } from 'dva'; import { Effect } from 'dva';
import isEqual from 'lodash/isEqual'; import isEqual from 'lodash/isEqual';
import memoizeOne from 'memoize-one'; import memoizeOne from 'memoize-one';
import { Reducer } from 'redux'; import { Reducer } from 'redux';
import { formatMessage } from 'umi-plugin-locale'; import { formatMessage } from 'umi-plugin-locale';
import { IRoute } from 'umi-types';
import defaultSettings from '../../config/defaultSettings'; import defaultSettings from '../../config/defaultSettings';
const { menu } = defaultSettings;
const { check } = Authorized;
// Conversion router to menu. // Conversion router to menu.
function formatter(data: any[], parentAuthority: string[], parentName: string): any[] { function formatter(
data: MenuDataItem[],
parentAuthority?: string[] | string,
parentName?: string,
): MenuDataItem[] {
return data return data
.filter(item => item.name && item.path)
.map(item => { .map(item => {
if (!item.name || !item.path) { const locale = `${parentName || 'menu'}.${item.name!}`;
return null;
}
let locale = 'menu';
if (parentName) {
locale = `${parentName}.${item.name}`;
} else {
locale = `menu.${item.name}`;
}
// if enableMenuLocale use item.name, // if enableMenuLocale use item.name,
// close menu international // close menu international
const name = menu.disableLocal const name = defaultSettings.menu.disableLocal
? item.name ? item.name!
: formatMessage({ id: locale, defaultMessage: item.name }); : formatMessage({ id: locale, defaultMessage: item.name! });
const result = { const result: MenuDataItem = {
...item, ...item,
name, name,
locale, locale,
...@@ -40,55 +36,43 @@ function formatter(data: any[], parentAuthority: string[], parentName: string): ...@@ -40,55 +36,43 @@ function formatter(data: any[], parentAuthority: string[], parentName: string):
} }
delete result.routes; delete result.routes;
return result; return result;
}) });
.filter(item => item);
} }
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: SubMenuItem) => any = item => { const getSubMenu: (item: MenuDataItem) => MenuDataItem = item => {
// doc: add hideChildrenInMenu if (
if (item.children && !item.hideChildrenInMenu && item.children.some(child => child.name)) { Array.isArray(item.children) &&
return { !item.hideChildrenInMenu &&
...item, item.children.some(child => (child.name ? true : false))
children: filterMenuData(item.children), // eslint-disable-line ) {
}; const children = filterMenuData(item.children);
if (children.length) return { ...item, children };
} }
return item; return { ...item, children: void 0 };
}; };
/** /**
* filter menuData * filter menuData
*/ */
const filterMenuData: (menuData: SubMenuItem[]) => SubMenuItem[] = menuData => { const filterMenuData = (menuData: MenuDataItem[] = []): MenuDataItem[] => {
if (!menuData) {
return [];
}
return menuData return menuData
.filter(item => item.name && !item.hideInMenu) .filter(item => item.name && !item.hideInMenu)
.map(item => check(item.authority, getSubMenu(item), null)) .map(item => Authorized.check<any, any>(item.authority!, getSubMenu(item), null))
.filter(item => item); .filter(item => item);
}; };
/** /**
* 获取面包屑映射 * 获取面包屑映射
* @param ISubMenuItem[] menuData 菜单配置 * @param MenuDataItem[] menuData 菜单配置
*/ */
const getBreadcrumbNameMap: (menuData: SubMenuItem[]) => object = menuData => { const getBreadcrumbNameMap = (menuData: MenuDataItem[]) => {
const routerMap = {}; const routerMap: { [key: string]: MenuDataItem } = {};
const flattenMenuData: (data: MenuDataItem[]) => void = 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);
...@@ -104,8 +88,8 @@ const getBreadcrumbNameMap: (menuData: SubMenuItem[]) => object = menuData => { ...@@ -104,8 +88,8 @@ const getBreadcrumbNameMap: (menuData: SubMenuItem[]) => object = menuData => {
const memoizeOneGetBreadcrumbNameMap = memoizeOne(getBreadcrumbNameMap, isEqual); const memoizeOneGetBreadcrumbNameMap = memoizeOne(getBreadcrumbNameMap, isEqual);
export interface MenuModelState { export interface MenuModelState {
menuData: any[]; menuData: MenuDataItem[];
routerData: any[]; routerData: IRoute[];
breadcrumbNameMap: object; breadcrumbNameMap: object;
} }
...@@ -116,9 +100,10 @@ export interface MenuModelType { ...@@ -116,9 +100,10 @@ export interface MenuModelType {
getMenuData: Effect; getMenuData: Effect;
}; };
reducers: { reducers: {
save: Reducer<any>; save: Reducer<MenuModelState>;
}; };
} }
const MenuModel: MenuModelType = { const MenuModel: MenuModelType = {
namespace: 'menu', namespace: 'menu',
......
...@@ -24,8 +24,8 @@ const updateTheme: (primaryColor?: string) => void = primaryColor => { ...@@ -24,8 +24,8 @@ const updateTheme: (primaryColor?: string) => void = primaryColor => {
const hideMessage = message.loading('正在编译主题!', 0); const hideMessage = message.loading('正在编译主题!', 0);
function buildIt() { function buildIt() {
if (!(window as any).less) { if (!(window as any).less) {
console.log('no less'); // tslint:disable-next-line no-console
return; return console.log('no less');
} }
setTimeout(() => { setTimeout(() => {
(window as any).less (window as any).less
......
...@@ -2,18 +2,20 @@ import { query as queryUsers, queryCurrent } from '@/services/user'; ...@@ -2,18 +2,20 @@ import { query as queryUsers, queryCurrent } from '@/services/user';
import { Effect } from 'dva'; import { Effect } from 'dva';
import { Reducer } from 'redux'; import { Reducer } from 'redux';
export interface CurrentUser {
avatar?: string;
name?: string;
title?: string;
group?: string;
signature?: string;
geographic?: any;
tags?: any[];
unreadCount?: number;
}
export interface UserModelState { export interface UserModelState {
list: any[]; list: any[];
currentUser: { currentUser: CurrentUser;
avatar?: string;
name?: string;
title?: string;
group?: string;
signature?: string;
geographic?: any;
tags?: any[];
unreadCount?: number;
};
} }
export interface UserModelType { export interface UserModelType {
......
import Authorized from '@/utils/Authorized'; import Authorized from '@/utils/Authorized';
import { ConnectProps, ConnectState, UserModelState } from '@/models/connect';
import { connect } from 'dva'; import { connect } from 'dva';
import pathToRegexp from 'path-to-regexp'; 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 { UserModelState } from '../models/user'; import { IRoute } from 'umi-types';
interface AuthComponentProps { interface AuthComponentProps extends ConnectProps {
location: Location; location: Location;
routerData: any[]; routerData: IRoute[];
user: UserModelState; user: UserModelState;
} }
const AuthComponent: React.SFC<AuthComponentProps> = ({ children, location, routerData, user }) => { const getRouteAuthority = (path: string, routeData: IRoute[]) => {
let authorities: string[] | string | undefined = void 0;
routeData.forEach(route => {
// match prefix
if (pathToRegexp(`${route.path}(.*)`).test(path)) {
authorities = route.authority || authorities;
// get children authority recursively
if (route.routes) {
authorities = getRouteAuthority(path, route.routes) || authorities;
}
}
});
return authorities;
};
const AuthComponent: React.FC<AuthComponentProps> = ({ children, location, routerData, user }) => {
const { currentUser } = user; const { currentUser } = user;
const isLogin = currentUser && currentUser.name; 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;
// get children authority recursively
if (route.routes) {
authorities = getRouteAuthority(path, route.routes) || authorities;
}
}
});
return authorities;
};
return ( return (
<Authorized <Authorized
authority={getRouteAuthority(location.pathname, routerData)} authority={getRouteAuthority(location.pathname, routerData)!}
noMatch={isLogin ? <Redirect to="/exception/403" /> : <Redirect to="/user/login" />} noMatch={isLogin ? <Redirect to="/exception/403" /> : <Redirect to="/user/login" />}
> >
{children} {children}
...@@ -39,7 +40,7 @@ const AuthComponent: React.SFC<AuthComponentProps> = ({ children, location, rout ...@@ -39,7 +40,7 @@ const AuthComponent: React.SFC<AuthComponentProps> = ({ children, location, rout
); );
}; };
export default connect(({ menu: menuModel, user }) => ({ export default connect(({ menu: menuModel, user }: ConnectState) => ({
routerData: menuModel.routerData, routerData: menuModel.routerData,
user, user,
}))(AuthComponent); }))(AuthComponent);
...@@ -38,11 +38,11 @@ workbox.routing.registerRoute(/\/api\//, workbox.strategies.networkFirst()); ...@@ -38,11 +38,11 @@ workbox.routing.registerRoute(/\/api\//, workbox.strategies.networkFirst());
*/ */
workbox.routing.registerRoute( workbox.routing.registerRoute(
/^https:\/\/gw.alipayobjects.com\//, /^https:\/\/gw.alipayobjects.com\//,
workbox.strategies.networkFirst() workbox.strategies.networkFirst(),
); );
workbox.routing.registerRoute( workbox.routing.registerRoute(
/^https:\/\/cdnjs.cloudflare.com\//, /^https:\/\/cdnjs.cloudflare.com\//,
workbox.strategies.networkFirst() workbox.strategies.networkFirst(),
); );
workbox.routing.registerRoute(/\/color.less/, workbox.strategies.networkFirst()); workbox.routing.registerRoute(/\/color.less/, workbox.strategies.networkFirst());
...@@ -58,8 +58,8 @@ addEventListener('message', event => { ...@@ -58,8 +58,8 @@ addEventListener('message', event => {
.skipWaiting() .skipWaiting()
.then( .then(
() => replyPort.postMessage({ error: null }), () => replyPort.postMessage({ error: null }),
error => replyPort.postMessage({ error }) error => replyPort.postMessage({ error }),
) ),
); );
} }
}); });
...@@ -9,4 +9,33 @@ declare module '*.jpeg'; ...@@ -9,4 +9,33 @@ declare module '*.jpeg';
declare module '*.gif'; declare module '*.gif';
declare module '*.bmp'; declare module '*.bmp';
declare module '*.tiff'; declare module '*.tiff';
declare module 'rc-animate';
declare module 'omit.js';
declare module 'react-copy-to-clipboard';
declare var APP_TYPE: string; declare var APP_TYPE: string;
declare module 'ant-design-pro' {
import React from 'react';
import { INoticeIconProps } from 'ant-design-pro/lib/NoticeIcon';
import { INoticeIconTabProps } from 'ant-design-pro/lib/NoticeIcon/NoticeIconTab';
type PartialNoticeIconProps = {
[K in Exclude<keyof INoticeIconProps, 'locale'>]?: INoticeIconProps[K]
};
interface MixinNoticeIconProps extends PartialNoticeIconProps {
locale?: {
emptyText: string;
clear: string;
viewMore: string;
[key: string]: string;
};
onViewMore?: (tabProps: INoticeIconProps) => void;
}
interface MixinNoticeIconTabProps extends Partial<INoticeIconTabProps> {
showViewMore?: boolean;
}
class NoticeIconTab extends React.Component<MixinNoticeIconTabProps, any> {}
export class NoticeIcon extends React.Component<MixinNoticeIconProps, any> {
public static Tab: typeof NoticeIconTab;
}
export * from 'ant-design-pro/lib';
}
...@@ -3,7 +3,7 @@ import { getAuthority } from './authority'; ...@@ -3,7 +3,7 @@ import { getAuthority } from './authority';
describe('getAuthority should be strong', () => { describe('getAuthority should be strong', () => {
it('empty', () => { it('empty', () => {
expect(getAuthority(null)).toEqual(['admin']); // default value expect(getAuthority(null!)).toEqual(['admin']); // default value
}); });
it('string', () => { it('string', () => {
expect(getAuthority('admin')).toEqual(['admin']); expect(getAuthority('admin')).toEqual(['admin']);
......
...@@ -6,7 +6,7 @@ export function getAuthority(str?: string): any { ...@@ -6,7 +6,7 @@ export function getAuthority(str?: string): any {
// authorityString could be admin, "admin", ["admin"] // authorityString could be admin, "admin", ["admin"]
let authority; let authority;
try { try {
authority = JSON.parse(authorityString); authority = JSON.parse(authorityString!);
} catch (e) { } catch (e) {
authority = authorityString; authority = authorityString;
} }
......
...@@ -3,24 +3,22 @@ import memoizeOne from 'memoize-one'; ...@@ -3,24 +3,22 @@ import memoizeOne from 'memoize-one';
import pathToRegexp from 'path-to-regexp'; import pathToRegexp from 'path-to-regexp';
import { formatMessage } from 'umi-plugin-locale'; import { formatMessage } from 'umi-plugin-locale';
import defaultSettings from '../../config/defaultSettings'; import defaultSettings from '../../config/defaultSettings';
import { MenuDataItem } from '@/components/SiderMenu/BaseMenu';
const { menu, title } = defaultSettings; const { menu, title } = defaultSettings;
interface RouterData { export const matchParamsPath = (
name: string; pathname: string,
locale: string; breadcrumbNameMap: { [path: string]: MenuDataItem },
authority?: string[]; ): MenuDataItem => {
children?: any[];
icon?: string;
path: string;
}
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: string, breadcrumbNameMap: object): string => { const getPageTitle = (
pathname: string,
breadcrumbNameMap: { [path: string]: MenuDataItem },
): string => {
const currRouterData = matchParamsPath(pathname, breadcrumbNameMap); const currRouterData = matchParamsPath(pathname, breadcrumbNameMap);
if (!currRouterData) { if (!currRouterData) {
return title; return title;
...@@ -28,7 +26,7 @@ const getPageTitle = (pathname: string, breadcrumbNameMap: object): string => { ...@@ -28,7 +26,7 @@ const getPageTitle = (pathname: string, breadcrumbNameMap: object): string => {
const pageName = menu.disableLocal const pageName = menu.disableLocal
? currRouterData.name ? currRouterData.name
: formatMessage({ : formatMessage({
id: currRouterData.locale || currRouterData.name, id: currRouterData.locale || currRouterData.name!,
defaultMessage: currRouterData.name, defaultMessage: currRouterData.name,
}); });
......
...@@ -5,6 +5,12 @@ ...@@ -5,6 +5,12 @@
import { extend } from 'umi-request'; import { extend } from 'umi-request';
import { notification } from 'antd'; import { notification } from 'antd';
interface ResponseError<D = any> extends Error {
name: string;
data: D;
response: Response;
}
const codeMessage = { const codeMessage = {
200: '服务器成功返回请求的数据。', 200: '服务器成功返回请求的数据。',
201: '新建或修改数据成功。', 201: '新建或修改数据成功。',
...@@ -26,8 +32,8 @@ const codeMessage = { ...@@ -26,8 +32,8 @@ const codeMessage = {
/** /**
* 异常处理程序 * 异常处理程序
*/ */
const errorHandler = error => { const errorHandler = (error: ResponseError) => {
const { response = {} } = error; const { response = {} as Response } = error;
const errortext = codeMessage[response.status] || response.statusText; const errortext = codeMessage[response.status] || response.statusText;
const { status, url } = response; const { status, url } = response;
......
import 'jest';
import { isUrl } from './utils';
describe('isUrl tests', () => {
it('should return false for invalid and corner case inputs', () => {
expect(isUrl([] as any)).toBeFalsy();
expect(isUrl({} as any)).toBeFalsy();
expect(isUrl(false as any)).toBeFalsy();
expect(isUrl(true as any)).toBeFalsy();
expect(isUrl(NaN as any)).toBeFalsy();
expect(isUrl(null as any)).toBeFalsy();
expect(isUrl(void 0 as any)).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();
});
});
/* eslint no-useless-escape:0 import/prefer-default-export:0 */ /* eslint no-useless-escape:0 import/prefer-default-export:0 */
const reg = /(((^https?:(?:\/\/)?)(?:[-;:&=\+\$,\w]+@)?[A-Za-z0-9.-]+(?::\d+)?|(?:www.|[-;:&=\+\$,\w]+@)[A-Za-z0-9.-]+)((?:\/[\+~%\/.\w-_]*)?\??(?:[-\+=&;%@.\w_]*)#?(?:[\w]*))?)$/; const reg = /(((^https?:(?:\/\/)?)(?:[-;:&=\+\$,\w]+@)?[A-Za-z0-9.-]+(?::\d+)?|(?:www.|[-;:&=\+\$,\w]+@)[A-Za-z0-9.-]+)((?:\/[\+~%\/.\w-_]*)?\??(?:[-\+=&;%@.\w_]*)#?(?:[\w]*))?)$/;
export function isUrl(path) { export function isUrl(path: string) {
return reg.test(path); return reg.test(path);
} }
...@@ -2,8 +2,8 @@ ...@@ -2,8 +2,8 @@
"compilerOptions": { "compilerOptions": {
"outDir": "build/dist", "outDir": "build/dist",
"module": "esnext", "module": "esnext",
"target": "es2016", "target": "esnext",
"lib": ["es6", "dom"], "lib": ["esnext", "dom"],
"sourceMap": true, "sourceMap": true,
"baseUrl": ".", "baseUrl": ".",
"jsx": "react", "jsx": "react",
...@@ -16,7 +16,7 @@ ...@@ -16,7 +16,7 @@
"noUnusedLocals": true, "noUnusedLocals": true,
"allowJs": true, "allowJs": true,
"experimentalDecorators": true, "experimentalDecorators": true,
"strictNullChecks": true, "strict": true,
"paths": { "paths": {
"@/*": ["./src/*"] "@/*": ["./src/*"]
} }
......
{
"extends": ["tslint:latest", "tslint-react", "tslint-config-prettier"],
"rules": {
"no-var-requires": false,
"no-submodule-imports": false,
"object-literal-sort-keys": false,
"jsx-no-lambda": false,
"no-implicit-dependencies": false,
"no-console": false,
"member-access": false,
"prefer-conditional-expression": false
}
}
defaultSeverity: error
extends:
- tslint-react
- tslint-eslint-rules
- tslint-config-prettier
jsRules:
rules:
class-name: true
eofline: true
forin: true
jsdoc-format: false
label-position: true
member-ordering:
- true
- order: statics-first
new-parens: true
no-arg: true
no-bitwise: true
no-conditional-assignment: true
no-consecutive-blank-lines: true
no-console:
- true
- debug
- info
- log
- time
- timeEnd
- trace
- warn
no-construct: true
no-debugger: true
no-duplicate-variable: true
no-eval: true
no-internal-module: true
no-multi-spaces: true
no-namespace: true
no-reference: true
no-shadowed-variable: true
no-string-literal: true
no-trailing-whitespace: true
no-unused-expression: true
no-var-keyword: true
one-variable-per-declaration:
- true
- ignore-for-loop
prefer-const:
- true
- destructuring: all
radix: true
space-in-parens: true
switch-default: true
trailing-comma:
- true
- singleline: never
multiline: always
esSpecCompliant: true
triple-equals:
- true
- allow-null-check
typedef-whitespace:
- true
- call-signature: nospace
index-signature: nospace
parameter: nospace
property-declaration: nospace
variable-declaration: nospace
- call-signature: onespace
index-signature: onespace
parameter: onespace
property-declaration: onespace
variable-declaration: onespace
use-isnan: true
variable-name:
- true
- allow-leading-underscore
- ban-keywords
- check-format
- allow-pascal-case
jsx-no-lambda: false
jsx-no-string-ref: false
jsx-boolean-value:
- true
- never
jsx-no-multiline-js: false
whitespace:
- true
- check-branch
- check-decl
- check-operator
- check-module
- check-separator
- check-rest-spread
- check-type
- check-type-operator
- check-preblock
declare module 'slash2';
declare module 'antd-pro-merge-less';
declare module 'antd-theme-webpack-plugin';
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