소스 검색

[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`
何乐 6 년 전
부모
커밋
0bc812b763
45개의 변경된 파일758개의 추가작업 그리고 548개의 파일을 삭제
  1. 2 0
      .eslintrc.js
  2. 1 1
      .prettierrc
  3. 3 4
      config/config.ts
  4. 39 14
      config/defaultSettings.ts
  5. 15 6
      package.json
  6. 7 6
      src/app.ts
  7. 23 30
      src/components/GlobalHeader/RightContent.tsx
  8. 14 10
      src/components/GlobalHeader/index.tsx
  9. 7 14
      src/components/HeaderDropdown/index.tsx
  10. 15 14
      src/components/HeaderSearch/index.tsx
  11. 1 1
      src/components/PageLoading/index.tsx
  12. 3 4
      src/components/SelectLang/index.tsx
  13. 3 2
      src/components/SettingDrawer/BlockCheckbox.tsx
  14. 40 47
      src/components/SettingDrawer/ThemeColor.tsx
  15. 29 33
      src/components/SettingDrawer/index.tsx
  16. 57 46
      src/components/SiderMenu/BaseMenu.tsx
  17. 30 24
      src/components/SiderMenu/SiderMenu.tsx
  18. 8 16
      src/components/SiderMenu/SiderMenuUtils.ts
  19. 9 12
      src/components/SiderMenu/index.tsx
  20. 14 47
      src/components/TopNavHeader/index.tsx
  21. 18 0
      src/components/_utils/pathTools.test.ts
  22. 1 1
      src/components/_utils/pathTools.ts
  23. 6 5
      src/global.tsx
  24. 28 31
      src/layouts/BasicLayout.tsx
  25. 2 6
      src/layouts/BlankLayout.tsx
  26. 29 29
      src/layouts/Header.tsx
  27. 9 13
      src/layouts/UserLayout.tsx
  28. 61 0
      src/models/connect.d.ts
  29. 20 12
      src/models/global.ts
  30. 34 49
      src/models/menu.ts
  31. 2 2
      src/models/setting.ts
  32. 12 10
      src/models/user.ts
  33. 22 21
      src/pages/Authorized.tsx
  34. 4 4
      src/service-worker.js
  35. 29 0
      src/typings.d.ts
  36. 1 1
      src/utils/authority.test.ts
  37. 1 1
      src/utils/authority.ts
  38. 11 13
      src/utils/getPageTitle.ts
  39. 8 2
      src/utils/request.ts
  40. 38 0
      src/utils/utils.test.ts
  41. 1 1
      src/utils/utils.ts
  42. 3 3
      tsconfig.json
  43. 0 13
      tslint.json
  44. 95 0
      tslint.yml
  45. 3 0
      typings.d.ts

+ 2 - 0
.eslintrc.js

@@ -35,5 +35,7 @@ module.exports = {
   },
   settings: {
     polyfills: ['fetch', 'promises', 'url'],
+    // support import modules from TypeScript files in JavaScript files
+    'import/resolver': { node: { extensions: ['.js', '.ts', '.tsx'] } },
   },
 };

+ 1 - 1
.prettierrc

@@ -1,6 +1,6 @@
 {
   "singleQuote": true,
-  "trailingComma": "es5",
+  "trailingComma": "all",
   "printWidth": 100,
   "overrides": [
     {

+ 3 - 4
config/config.ts

@@ -1,7 +1,7 @@
 // https://umijs.org/config/
-import os from 'os';
+// import os from 'os';
 import slash from 'slash2';
-import { IPlugin } from 'umi-types';
+import { IPlugin, IConfig } from 'umi-types';
 import defaultSettings from './defaultSettings';
 import webpackPlugin from './plugin.config';
 
@@ -144,6 +144,5 @@ export default {
   manifest: {
     basePath: '/',
   },
-
   chainWebpack: webpackPlugin,
-};
+} as IConfig;

+ 39 - 14
config/defaultSettings.ts

@@ -1,36 +1,61 @@
-export declare type SiderTheme = 'light' | 'dark';
+import { MenuTheme } from 'antd/es/menu';
+
+export type ContentWidth = 'Fluid' | 'Fixed';
 
 export interface DefaultSettings {
-  navTheme: string | SiderTheme;
+  /**
+   * theme for nav menu
+   */
+  navTheme: MenuTheme;
+  /**
+   * primary color of ant design
+   */
   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;
+  /**
+   * auto hide header
+   */
   autoHideHeader: boolean;
+  /**
+   * sticky siderbar
+   */
   fixSiderbar: boolean;
   menu: { disableLocal: boolean };
   title: string;
   pwa: boolean;
+  /**
+   * your iconfont Symbol Scrip Url
+   * eg:`//at.alicdn.com/t/font_1039637_btcrd5co4w.js`
+   * 注意:如果需要图标多色,Iconfont图标项目里要进行批量去色处理
+   */
   iconfontUrl: string;
   colorWeak: boolean;
 }
 
 export default {
-  navTheme: 'dark', // theme for nav menu
-  primaryColor: '#1890FF', // primary color of ant design
-  layout: 'sidemenu', // nav menu position: sidemenu or topmenu
-  contentWidth: 'Fluid', // layout of content: Fluid or Fixed, only works when layout is topmenu
-  fixedHeader: false, // sticky header
-  autoHideHeader: false, // auto hide header
-  fixSiderbar: false, // sticky siderbar
+  navTheme: 'dark',
+  primaryColor: '#1890FF',
+  layout: 'sidemenu',
+  contentWidth: 'Fluid',
+  fixedHeader: false,
+  autoHideHeader: false,
+  fixSiderbar: false,
   colorWeak: false,
   menu: {
     disableLocal: false,
   },
   title: 'Ant Design Pro',
   pwa: true,
-  // your iconfont Symbol Scrip Url
-  // eg://at.alicdn.com/t/font_1039637_btcrd5co4w.js
-  // 注意:如果需要图标多色,Iconfont图标项目里要进行批量去色处理
   iconfontUrl: '',
 } as DefaultSettings;

+ 15 - 6
package.json

@@ -11,12 +11,15 @@
     "dev:no-mock": "cross-env MOCK=none umi dev",
     "build": "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:prettier": "check-prettier lint",
-    "lint": "eslint --ext .js src tests && npm run lint:style && npm run lint:prettier",
-    "lint:fix": "eslint --fix --ext .js src tests && stylelint --fix 'src/**/*.less' --syntax less",
+    "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 && tslint --fix -p . -c tslint.yml && stylelint --fix 'src/**/*.less' --syntax less",
     "lint-staged": "lint-staged",
     "lint-staged:js": "eslint --ext .js",
+    "lint-staged:ts": "tslint",
     "test": "umi test",
     "test:component": "umi test ./src/components",
     "test:all": "node ./tests/run-tests.js",
@@ -46,14 +49,17 @@
     "react-container-query": "^0.11.0",
     "react-copy-to-clipboard": "^5.0.1",
     "react-document-title": "^2.0.3",
-    "react-media": "^1.8.0",
     "react-media-hook2": "^1.0.2",
-    "umi-request": "^1.0.0",
-    "umi-types": "^0.2.0"
+    "umi-request": "^1.0.0"
   },
   "devDependencies": {
+    "@types/classnames": "^2.2.7",
+    "@types/enzyme": "^3.9.0",
     "@types/jest": "^24.0.11",
+    "@types/lodash": "^4.14.122",
+    "@types/memoize-one": "^4.1.0",
     "@types/react": "^16.8.1",
+    "@types/react-document-title": "^2.0.3",
     "@types/react-dom": "^16.0.11",
     "antd-pro-merge-less": "^1.0.0",
     "antd-theme-webpack-plugin": "^1.2.0",
@@ -90,11 +96,13 @@
     "stylelint-order": "^2.0.0",
     "tslint": "^5.12.1",
     "tslint-config-prettier": "^1.17.0",
+    "tslint-eslint-rules": "^5.4.0",
     "tslint-react": "^3.6.0",
     "umi": "^2.4.4",
     "umi-plugin-ga": "^1.1.3",
     "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": {
     "puppeteer": "^1.12.1"
@@ -105,6 +113,7 @@
       "git add"
     ],
     "**/*.{js,jsx}": "npm run lint-staged:js",
+    "**/*.{ts,tsx}": "npm run lint-staged:ts",
     "**/*.less": "stylelint --syntax less"
   },
   "engines": {

+ 7 - 6
src/app.ts

@@ -1,8 +1,9 @@
 import fetch from 'dva/fetch';
+import { IRoute } from 'umi-types';
 
 export const dva = {
   config: {
-    onError(err) {
+    onError(err: ErrorEvent) {
       err.preventDefault();
     },
   },
@@ -10,7 +11,7 @@ export const dva = {
 
 let authRoutes = {};
 
-function ergodicRoutes(routes, authKey, authority) {
+function ergodicRoutes(routes: IRoute[], authKey: string, authority: string | string[]) {
   routes.forEach(element => {
     if (element.path === authKey) {
       if (!element.authority) element.authority = []; // eslint-disable-line
@@ -22,14 +23,14 @@ function ergodicRoutes(routes, authKey, authority) {
   });
 }
 
-export function patchRoutes(routes) {
+export function patchRoutes(routes: IRoute[]) {
   Object.keys(authRoutes).map(authKey =>
-    ergodicRoutes(routes, authKey, authRoutes[authKey].authority)
+    ergodicRoutes(routes, authKey, authRoutes[authKey].authority),
   );
   (window as any).g_routes = routes;
 }
 
-export function render(oldRender) {
+export function render(oldRender: Function) {
   fetch('/api/auth_routes')
     .then(res => res.json())
     .then(
@@ -39,6 +40,6 @@ export function render(oldRender) {
       },
       () => {
         oldRender();
-      }
+      },
     );
 }

+ 23 - 30
src/components/GlobalHeader/RightContent.tsx

@@ -1,3 +1,6 @@
+import { ConnectProps } from '@/models/connect';
+import { NoticeItem } from '@/models/global';
+import { CurrentUser } from '@/models/user';
 import React, { Component } from 'react';
 import { FormattedMessage, formatMessage } from 'umi-plugin-locale';
 import { Spin, Tag, Menu, Icon, Avatar, Tooltip, message } from 'antd';
@@ -10,30 +13,20 @@ import HeaderDropdown from '../HeaderDropdown';
 import SelectLang from '../SelectLang';
 import styles from './index.less';
 
-export declare type SiderTheme = 'light' | 'dark';
+export type SiderTheme = 'light' | 'dark';
 
-interface GlobalHeaderRightProps {
-  notices?: any[];
-  dispatch?: (args: any) => void;
-  // wait for https://github.com/umijs/umi/pull/2036
-  currentUser?: {
-    avatar?: string;
-    name?: string;
-    title?: string;
-    group?: string;
-    signature?: string;
-    geographic?: any;
-    tags?: any[];
-    unreadCount: number;
-  };
+export interface GlobalHeaderRightProps extends ConnectProps {
+  notices?: NoticeItem[];
+  currentUser?: CurrentUser;
   fetchingNotices?: boolean;
   onNoticeVisibleChange?: (visible: boolean) => void;
   onMenuClick?: (param: ClickParam) => void;
   onNoticeClear?: (tabName: string) => void;
   theme?: SiderTheme;
 }
+
 export default class GlobalHeaderRight extends Component<GlobalHeaderRightProps> {
-  getNoticeData() {
+  getNoticeData = (): { [key: string]: NoticeItem[] } => {
     const { notices = [] } = this.props;
     if (notices.length === 0) {
       return {};
@@ -41,7 +34,7 @@ export default class GlobalHeaderRight extends Component<GlobalHeaderRightProps>
     const newNotices = notices.map(notice => {
       const newNotice = { ...notice };
       if (newNotice.datetime) {
-        newNotice.datetime = moment(notice.datetime).fromNow();
+        newNotice.datetime = moment(notice.datetime as string).fromNow();
       }
       if (newNotice.id) {
         newNotice.key = newNotice.id;
@@ -62,10 +55,10 @@ export default class GlobalHeaderRight extends Component<GlobalHeaderRightProps>
       return newNotice;
     });
     return groupBy(newNotices, 'type');
-  }
+  };
 
-  getUnreadData: (noticeData: object) => any = noticeData => {
-    const unreadMsg = {};
+  getUnreadData = (noticeData: { [key: string]: NoticeItem[] }) => {
+    const unreadMsg: { [key: string]: number } = {};
     Object.entries(noticeData).forEach(([key, value]) => {
       if (!unreadMsg[key]) {
         unreadMsg[key] = 0;
@@ -77,10 +70,10 @@ export default class GlobalHeaderRight extends Component<GlobalHeaderRightProps>
     return unreadMsg;
   };
 
-  changeReadState = clickedItem => {
+  changeReadState = (clickedItem: NoticeItem) => {
     const { id } = clickedItem;
     const { dispatch } = this.props;
-    dispatch({
+    dispatch!({
       type: 'global/changeNoticeReadState',
       payload: id,
     });
@@ -133,10 +126,10 @@ export default class GlobalHeaderRight extends Component<GlobalHeaderRightProps>
             formatMessage({ id: 'component.globalHeader.search.example3' }),
           ]}
           onSearch={value => {
-            console.log('input', value); // eslint-disable-line
+            console.log('input', value); // tslint:disable-line no-console
           }}
           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' })}>
@@ -152,23 +145,23 @@ export default class GlobalHeaderRight extends Component<GlobalHeaderRightProps>
 
         <NoticeIcon
           className={styles.action}
-          count={currentUser.unreadCount}
+          count={currentUser && currentUser.unreadCount}
           onItemClick={(item, tabProps) => {
-            console.log(item, tabProps); // eslint-disable-line
-            this.changeReadState(item);
+            console.log(item, tabProps); // tslint:disable-line no-console
+            this.changeReadState(item as NoticeItem);
           }}
           loading={fetchingNotices}
           locale={{
             emptyText: formatMessage({ id: 'component.noticeIcon.empty' }),
             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' }),
             message: formatMessage({ id: 'component.globalHeader.message' }),
             event: formatMessage({ id: 'component.globalHeader.event' }),
           }}
           onClear={onNoticeClear}
           onPopupVisibleChange={onNoticeVisibleChange}
-          onViewMore={() => message.info('Click on view more')} // todo:onViewMore?: (tabProps: INoticeIconProps) => void;
+          onViewMore={() => message.info('Click on view more')}
           clearClose
         >
           <NoticeIcon.Tab
@@ -196,7 +189,7 @@ export default class GlobalHeaderRight extends Component<GlobalHeaderRightProps>
             showViewMore
           />
         </NoticeIcon>
-        {currentUser.name ? (
+        {currentUser && currentUser.name ? (
           <HeaderDropdown overlay={menu}>
             <span className={`${styles.action} ${styles.account}`}>
               <Avatar

+ 14 - 10
src/components/GlobalHeader/index.tsx

@@ -3,31 +3,35 @@ import { Icon } from 'antd';
 import Link from 'umi/link';
 import debounce from 'lodash/debounce';
 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;
   onCollapse?: (collapsed: boolean) => void;
   isMobile?: boolean;
   logo?: string;
-  onNoticeClear?: (type: string) => void;
-  onMenuClick?: ({ key: string }) => void;
-  onNoticeVisibleChange?: (b: boolean) => void;
 }
 
 export default class GlobalHeader extends Component<GlobalHeaderProps> {
-  componentWillUnmount() {
-    this.triggerResizeEvent.cancel();
-  }
   triggerResizeEvent = debounce(() => {
-    // eslint-disable-line
     const event = document.createEvent('HTMLEvents');
     event.initEvent('resize', true, false);
     window.dispatchEvent(event);
   });
+  componentWillUnmount() {
+    this.triggerResizeEvent.cancel();
+  }
   toggle = () => {
     const { collapsed, onCollapse } = this.props;
-    onCollapse(!collapsed);
+    if (onCollapse) onCollapse(!collapsed);
     this.triggerResizeEvent();
   };
   render() {

+ 7 - 14
src/components/HeaderDropdown/index.tsx

@@ -1,22 +1,15 @@
-import React, { Component } from 'react';
+import React from 'react';
 import { Dropdown } from 'antd';
+import { DropDownProps } from 'antd/es/dropdown';
 import classNames from 'classnames';
 import styles from './index.less';
 
-declare type OverlayFunc = () => React.ReactNode;
-
-interface HeaderDropdownProps {
+export interface HeaderDropdownProps extends DropDownProps {
   overlayClassName?: string;
-  overlay: React.ReactNode | OverlayFunc;
-  placement?: 'bottomLeft' | 'bottomRight' | 'topLeft' | 'topCenter' | 'topRight' | 'bottomCenter';
 }
 
-export default class HeaderDropdown extends Component<HeaderDropdownProps> {
-  render() {
-    const { overlayClassName, ...props } = this.props;
+const HeaderDropdown: React.FC<HeaderDropdownProps> = ({ overlayClassName: cls, ...restProps }) => (
+  <Dropdown overlayClassName={classNames(styles.container, cls)} {...restProps} />
+);
 
-    return (
-      <Dropdown overlayClassName={classNames(styles.container, overlayClassName)} {...props} />
-    );
-  }
-}
+export default HeaderDropdown;

+ 15 - 14
src/components/HeaderSearch/index.tsx

@@ -1,13 +1,12 @@
 import React, { Component } from 'react';
 import { Input, Icon, AutoComplete } from 'antd';
-import InputProps from 'antd/es/input';
-
+import { DataSourceItemType } from 'antd/es/auto-complete';
 import classNames from 'classnames';
 import Debounce from 'lodash-decorators/debounce';
 import Bind from 'lodash-decorators/bind';
 import styles from './index.less';
 
-interface HeaderSearchProps {
+export interface HeaderSearchProps {
   onPressEnter: (value: string) => void;
   onSearch: (value: string) => void;
   onChange: (value: string) => void;
@@ -15,7 +14,7 @@ interface HeaderSearchProps {
   className: string;
   placeholder: string;
   defaultActiveFirstOption: boolean;
-  dataSource: any[];
+  dataSource: DataSourceItemType[];
   defaultOpen: boolean;
   open?: boolean;
 }
@@ -24,6 +23,7 @@ interface HeaderSearchState {
   value: string;
   searchMode: boolean;
 }
+
 export default class HeaderSearch extends Component<HeaderSearchProps, HeaderSearchState> {
   static defaultProps = {
     defaultActiveFirstOption: false,
@@ -37,7 +37,7 @@ export default class HeaderSearch extends Component<HeaderSearchProps, HeaderSea
     onVisibleChange: () => {},
   };
 
-  static getDerivedStateFromProps(props) {
+  static getDerivedStateFromProps(props: HeaderSearchProps) {
     if ('open' in props) {
       return {
         searchMode: props.open,
@@ -46,9 +46,10 @@ export default class HeaderSearch extends Component<HeaderSearchProps, HeaderSea
     return null;
   }
 
-  timeout: NodeJS.Timeout;
-  input: InputProps;
-  constructor(props) {
+  private timeout: NodeJS.Timeout = null!;
+  private inputRef: Input | null = null;
+
+  constructor(props: HeaderSearchProps) {
     super(props);
     this.state = {
       searchMode: props.defaultOpen,
@@ -60,7 +61,7 @@ export default class HeaderSearch extends Component<HeaderSearchProps, HeaderSea
     clearTimeout(this.timeout);
   }
 
-  onKeyDown = e => {
+  onKeyDown = (e: React.KeyboardEvent) => {
     if (e.key === 'Enter') {
       const { onPressEnter } = this.props;
       const { value } = this.state;
@@ -70,7 +71,7 @@ export default class HeaderSearch extends Component<HeaderSearchProps, HeaderSea
     }
   };
 
-  onChange = value => {
+  onChange = (value: string) => {
     const { onSearch, onChange } = this.props;
     this.setState({ value });
     if (onSearch) {
@@ -86,8 +87,8 @@ export default class HeaderSearch extends Component<HeaderSearchProps, HeaderSea
     onVisibleChange(true);
     this.setState({ searchMode: true }, () => {
       const { searchMode } = this.state;
-      if (searchMode) {
-        this.input.focus();
+      if (searchMode && this.inputRef) {
+        this.inputRef.focus();
       }
     });
   };
@@ -135,11 +136,11 @@ export default class HeaderSearch extends Component<HeaderSearchProps, HeaderSea
           {...restProps}
           className={inputClass}
           value={value}
-          onChange={this.onChange}
+          onChange={this.onChange as any}
         >
           <Input
             ref={node => {
-              this.input = node;
+              this.inputRef = node;
             }}
             aria-label={placeholder}
             placeholder={placeholder}

+ 1 - 1
src/components/PageLoading/index.tsx

@@ -3,7 +3,7 @@ import { Spin } from 'antd';
 
 // loading components from code split
 // https://umijs.org/plugin/umi-plugin-react.html#dynamicimport
-const PageLoding: React.SFC = () => (
+const PageLoding: React.FC = () => (
   <div style={{ paddingTop: 100, textAlign: 'center' }}>
     <Spin size="large" />
   </div>

+ 3 - 4
src/components/SelectLang/index.tsx

@@ -1,6 +1,7 @@
 import React from 'react';
 import { formatMessage, setLocale, getLocale } from 'umi-plugin-locale';
 import { Menu, Icon } from 'antd';
+import { ClickParam } from 'antd/es/menu';
 import classNames from 'classnames';
 import HeaderDropdown from '../HeaderDropdown';
 import styles from './index.less';
@@ -8,12 +9,10 @@ import styles from './index.less';
 interface SelectLangProps {
   className?: string;
 }
-const SelectLang: React.SFC<SelectLangProps> = props => {
+const SelectLang: React.FC<SelectLangProps> = props => {
   const { className } = props;
   const selectedLang = getLocale();
-  const changeLang = ({ key }) => {
-    setLocale(key);
-  };
+  const changeLang = ({ key }: ClickParam) => setLocale(key);
   const locales = ['zh-CN', 'zh-TW', 'en-US', 'pt-BR'];
   const languageLabels = {
     'zh-CN': '简体中文',

+ 3 - 2
src/components/SettingDrawer/BlockCheckbox.tsx

@@ -2,12 +2,13 @@ import React from 'react';
 import { Tooltip, Icon } from 'antd';
 import style from './index.less';
 
-interface BlockChecboxProps {
+export interface BlockChecboxProps {
   value: string;
   onChange: (key: string) => void;
   list: any[];
 }
-const BlockChecbox: React.SFC<BlockChecboxProps> = ({ value, onChange, list }) => (
+
+const BlockChecbox: React.FC<BlockChecboxProps> = ({ value, onChange, list }) => (
   <div className={style.blockChecbox} key={value}>
     {list.map(item => (
       <Tooltip title={item.title} key={item.key}>

+ 40 - 47
src/components/SettingDrawer/ThemeColor.tsx

@@ -3,68 +3,61 @@ import { Tooltip, Icon } from 'antd';
 import { formatMessage } from 'umi-plugin-locale';
 import styles from './ThemeColor.less';
 
-interface TagProps {
+export interface TagProps {
   color: string;
   check: boolean;
   className?: string;
   onClick?: () => void;
 }
-const Tag: React.SFC<TagProps> = ({ color, check, ...rest }) => (
-  <div
-    {...rest}
-    style={{
-      backgroundColor: color,
-    }}
-  >
+
+const Tag: React.FC<TagProps> = ({ color, check, ...rest }) => (
+  <div {...rest} style={{ backgroundColor: color }}>
     {check ? <Icon type="check" /> : ''}
   </div>
 );
 
-interface ThemeColorProps {
+export interface ThemeColorProps {
   colors?: any[];
   title?: string;
   value: string;
   onChange: (color: string) => void;
 }
 
-const ThemeColor: React.SFC<ThemeColorProps> = ({ colors, title, value, onChange }) => {
-  let colorList = colors;
-  if (!colors) {
-    colorList = [
-      {
-        key: 'dust',
-        color: '#F5222D',
-      },
-      {
-        key: 'volcano',
-        color: '#FA541C',
-      },
-      {
-        key: 'sunset',
-        color: '#FAAD14',
-      },
-      {
-        key: 'cyan',
-        color: '#13C2C2',
-      },
-      {
-        key: 'green',
-        color: '#52C41A',
-      },
-      {
-        key: 'daybreak',
-        color: '#1890FF',
-      },
-      {
-        key: 'geekblue',
-        color: '#2F54EB',
-      },
-      {
-        key: 'purple',
-        color: '#722ED1',
-      },
-    ];
-  }
+const ThemeColor: React.FC<ThemeColorProps> = ({ colors, title, value, onChange }) => {
+  const colorList = colors || [
+    {
+      key: 'dust',
+      color: '#F5222D',
+    },
+    {
+      key: 'volcano',
+      color: '#FA541C',
+    },
+    {
+      key: 'sunset',
+      color: '#FAAD14',
+    },
+    {
+      key: 'cyan',
+      color: '#13C2C2',
+    },
+    {
+      key: 'green',
+      color: '#52C41A',
+    },
+    {
+      key: 'daybreak',
+      color: '#1890FF',
+    },
+    {
+      key: 'geekblue',
+      color: '#2F54EB',
+    },
+    {
+      key: 'purple',
+      color: '#722ED1',
+    },
+  ];
   return (
     <div className={styles.themeColor}>
       <h3 className={styles.title}>{title}</h3>

+ 29 - 33
src/components/SettingDrawer/index.tsx

@@ -1,3 +1,4 @@
+import { ConnectProps, ConnectState, SettingModelState } from '@/models/connect';
 import React, { Component } from 'react';
 import { Select, message, Drawer, List, Switch, Divider, Icon, Button, Alert, Tooltip } from 'antd';
 import { formatMessage } from 'umi-plugin-locale';
@@ -7,7 +8,6 @@ import omit from 'omit.js';
 import styles from './index.less';
 import ThemeColor from './ThemeColor';
 import BlockCheckbox from './BlockCheckbox';
-import { DefaultSettings } from '../../../config/defaultSettings';
 
 const { Option } = Select;
 interface BodyProps {
@@ -15,34 +15,37 @@ interface BodyProps {
   style?: React.CSSProperties;
 }
 
-const Body: React.SFC<BodyProps> = ({ children, title, style }) => (
-  <div
-    style={{
-      ...style,
-      marginBottom: 24,
-    }}
-  >
+const Body: React.FC<BodyProps> = ({ children, title, style }) => (
+  <div style={{ ...style, marginBottom: 24 }}>
     <h3 className={styles.title}>{title}</h3>
     {children}
   </div>
 );
 
-interface SettingDrawerProps {
-  setting?: DefaultSettings;
-  dispatch?: (args: any) => void;
+interface SettingItemProps {
+  title: React.ReactNode;
+  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> {
-  state = {
+  state: SettingDrawerState = {
     collapse: false,
   };
 
-  getLayoutSetting = () => {
-    const {
-      setting: { contentWidth, fixedHeader, layout, autoHideHeader, fixSiderbar },
-    } = this.props;
+  getLayoutSetting = (): SettingItemProps[] => {
+    const { setting } = this.props;
+    const { contentWidth, fixedHeader, layout, autoHideHeader, fixSiderbar } = setting!;
     return [
       {
         title: formatMessage({ id: 'app.setting.content-width' }),
@@ -101,9 +104,9 @@ class SettingDrawer extends Component<SettingDrawerProps, SettingDrawerState> {
     ];
   };
 
-  changeSetting = (key, value) => {
+  changeSetting = (key: string, value: any) => {
     const { setting } = this.props;
-    const nextState = { ...setting };
+    const nextState = { ...setting! };
     nextState[key] = value;
     if (key === 'layout') {
       nextState.contentWidth = value === 'topmenu' ? 'Fixed' : 'Fluid';
@@ -112,7 +115,7 @@ class SettingDrawer extends Component<SettingDrawerProps, SettingDrawerState> {
     }
     this.setState(nextState, () => {
       const { dispatch } = this.props;
-      dispatch({
+      dispatch!({
         type: 'setting/changeSetting',
         payload: this.state,
       });
@@ -124,7 +127,7 @@ class SettingDrawer extends Component<SettingDrawerProps, SettingDrawerState> {
     this.setState({ collapse: !collapse });
   };
 
-  renderLayoutSettingItem = item => {
+  renderLayoutSettingItem = (item: SettingItemProps) => {
     const action = React.cloneElement(item.action, {
       disabled: item.disabled,
     });
@@ -139,7 +142,7 @@ class SettingDrawer extends Component<SettingDrawerProps, SettingDrawerState> {
 
   render() {
     const { setting } = this.props;
-    const { navTheme, primaryColor, layout, colorWeak } = setting;
+    const { navTheme, primaryColor, layout, colorWeak } = setting!;
     const { collapse } = this.state;
     return (
       <Drawer
@@ -149,18 +152,10 @@ class SettingDrawer extends Component<SettingDrawerProps, SettingDrawerState> {
         placement="right"
         handler={
           <div className={styles.handle} onClick={this.togglerContent}>
-            <Icon
-              type={collapse ? 'close' : 'setting'}
-              style={{
-                color: '#fff',
-                fontSize: 20,
-              }}
-            />
+            <Icon type={collapse ? 'close' : 'setting'} style={{ color: '#fff', fontSize: 20 }} />
           </div>
         }
-        style={{
-          zIndex: 999,
-        }}
+        style={{ zIndex: 999 }}
       >
         <div className={styles.content}>
           <Body title={formatMessage({ id: 'app.setting.pagestyle' })}>
@@ -221,6 +216,7 @@ class SettingDrawer extends Component<SettingDrawerProps, SettingDrawerState> {
             <List.Item
               actions={[
                 <Switch
+                  key="Switch"
                   size="small"
                   checked={!!colorWeak}
                   onChange={checked => this.changeSetting('colorWeak', checked)}

+ 57 - 46
src/components/SiderMenu/BaseMenu.tsx

@@ -1,8 +1,8 @@
 import IconFont from '@/components/IconFont';
 import { isUrl } from '@/utils/utils';
 import { Icon, Menu } from 'antd';
+import { MenuMode, MenuTheme } from 'antd/es/menu';
 import classNames from 'classnames';
-import * as H from 'history';
 import React, { Component } from 'react';
 import Link from 'umi/link';
 import { urlToList } from '../_utils/pathTools';
@@ -16,7 +16,7 @@ const { SubMenu } = Menu;
 //   icon: 'icon-geren' #For Iconfont ,
 //   icon: 'http://demo.com/icon.png',
 //   icon: <Icon type="setting" />,
-const getIcon = icon => {
+const getIcon = (icon?: string | React.ReactNode) => {
   if (typeof icon === 'string') {
     if (isUrl(icon)) {
       return <Icon component={() => <img src={icon} alt="icon" className={styles.icon} />} />;
@@ -29,42 +29,55 @@ const getIcon = icon => {
   return icon;
 };
 
-export declare type CollapseType = 'clickTrigger' | 'responsive';
-export declare type SiderTheme = 'light' | 'dark';
-export declare type MenuMode =
-  | 'vertical'
-  | 'vertical-left'
-  | 'vertical-right'
-  | 'horizontal'
-  | 'inline';
+/**
+ * @type R: is route
+ */
+export interface MenuDataItem<R extends boolean = false> {
+  authority?: string[] | string;
+  children?: MenuDataItem[];
+  hideChildrenInMenu?: boolean;
+  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[];
-  location?: H.Location;
-  onCollapse?: (collapsed: boolean, type?: CollapseType) => void;
+  handleOpenChange?: (openKeys: string[]) => void;
   isMobile?: boolean;
-  openKeys?: any;
-  theme?: SiderTheme;
+  location?: Location;
+  menuData?: MenuDataItem[];
   mode?: MenuMode;
-  className?: string;
-  collapsed?: boolean;
-  handleOpenChange?: (openKeys: any[]) => void;
-  menuData?: any[];
-  style?: React.CSSProperties;
+  onCollapse?: (collapsed: boolean) => 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 => {
-    if (!menusData) {
-      return [];
-    }
+  getNavMenuItems = (menusData: MenuDataItem[] = []): React.ReactNode[] => {
     return menusData
       .filter(item => item.name && !item.hideInMenu)
       .map(item => this.getSubMenuOrItem(item))
@@ -72,18 +85,22 @@ export default class BaseMenu extends Component<BaseMenuProps, BaseMenuState> {
   };
 
   // Get the currently selected menu
-  getSelectedMenuKeys = pathname => {
+  getSelectedMenuKeys = (pathname: string): string[] => {
     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
    */
-  getSubMenuOrItem = item => {
-    // doc: add hideChildrenInMenu
-    if (item.children && !item.hideChildrenInMenu && item.children.some(child => child.name)) {
-      const { name } = item;
+  getSubMenuOrItem = (item: MenuDataItem): React.ReactNode => {
+    if (
+      Array.isArray(item.children) &&
+      !item.hideChildrenInMenu &&
+      item.children.some(child => (child.name ? true : false))
+    ) {
       return (
         <SubMenu
           title={
@@ -93,7 +110,7 @@ export default class BaseMenu extends Component<BaseMenuProps, BaseMenuState> {
                 <span>{name}</span>
               </span>
             ) : (
-              name
+              item.name
             )
           }
           key={item.path}
@@ -110,7 +127,7 @@ export default class BaseMenu extends Component<BaseMenuProps, BaseMenuState> {
    * Judge whether it is http link.return a or Link
    * @memberof SiderMenu
    */
-  getMenuItemPath = item => {
+  getMenuItemPath = (item: MenuDataItem) => {
     const { name } = item;
     const itemPath = this.conversionPath(item.path);
     const icon = getIcon(item.icon);
@@ -129,14 +146,8 @@ export default class BaseMenu extends Component<BaseMenuProps, BaseMenuState> {
       <Link
         to={itemPath}
         target={target}
-        replace={itemPath === location.pathname}
-        onClick={
-          isMobile
-            ? () => {
-                onCollapse(true);
-              }
-            : undefined
-        }
+        replace={itemPath === location!.pathname}
+        onClick={isMobile ? () => onCollapse!(true) : void 0}
       >
         {icon}
         <span>{name}</span>
@@ -144,7 +155,7 @@ export default class BaseMenu extends Component<BaseMenuProps, BaseMenuState> {
     );
   };
 
-  conversionPath = path => {
+  conversionPath = (path: string) => {
     if (path && path.indexOf('http') === 0) {
       return path;
     }
@@ -156,7 +167,7 @@ export default class BaseMenu extends Component<BaseMenuProps, BaseMenuState> {
       openKeys,
       theme,
       mode,
-      location: { pathname },
+      location,
       className,
       collapsed,
       handleOpenChange,
@@ -164,7 +175,7 @@ export default class BaseMenu extends Component<BaseMenuProps, BaseMenuState> {
       menuData,
     } = this.props;
     // 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) {
       selectedKeys = [openKeys[openKeys.length - 1]];
     }

+ 30 - 24
src/components/SiderMenu/SiderMenu.tsx

@@ -1,10 +1,10 @@
 import { Layout } from 'antd';
 import classNames from 'classnames';
-import * as H from 'history';
 import React, { Component, Suspense } from 'react';
 import Link from 'umi/link';
 import defaultSettings from '../../../config/defaultSettings';
 import PageLoading from '../PageLoading';
+import { BaseMenuProps } from './BaseMenu';
 import styles from './index.less';
 import { getDefaultCollapsedSubMenus } from './SiderMenuUtils';
 
@@ -13,38 +13,42 @@ const { Sider } = Layout;
 const { title } = defaultSettings;
 let firstMount: boolean = true;
 
-export declare type CollapseType = 'clickTrigger' | 'responsive';
-export declare type SiderTheme = 'light' | 'dark';
-
-interface SiderMenuProps {
-  menuData: any[];
-  location?: H.Location;
-  flatMenuKeys?: any[];
+export interface SiderMenuProps extends BaseMenuProps {
   logo?: string;
-  collapsed: boolean;
-  onCollapse: (collapsed: boolean, type?: CollapseType) => void;
   fixSiderbar?: boolean;
-  theme?: SiderTheme;
-  isMobile: boolean;
 }
 
 interface SiderMenuState {
-  openKeys: any;
+  pathname?: string;
+  openKeys?: string[];
   flatMenuKeysLen?: number;
 }
 
 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;
-    if (props.location.pathname !== pathname || props.flatMenuKeys.length !== flatMenuKeysLen) {
+    if (props.location!.pathname !== pathname || props.flatMenuKeys!.length !== flatMenuKeysLen) {
       return {
-        pathname: props.location.pathname,
-        flatMenuKeysLen: props.flatMenuKeys.length,
+        pathname: props.location!.pathname,
+        flatMenuKeysLen: props.flatMenuKeys!.length,
         openKeys: getDefaultCollapsedSubMenus(props),
       };
     }
     return null;
   }
+
   constructor(props: SiderMenuProps) {
     super(props);
     this.state = {
@@ -58,7 +62,7 @@ export default class SiderMenu extends Component<SiderMenuProps, SiderMenuState>
 
   isMainMenu: (key: string) => boolean = key => {
     const { menuData } = this.props;
-    return menuData.some(item => {
+    return menuData!.some(item => {
       if (key) {
         return item.key === key || item.path === key;
       }
@@ -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;
-    this.setState({
-      openKeys: moreThanOne ? [openKeys.pop()] : [...openKeys],
-    });
+    if (moreThanOne) {
+      this.setState({ openKeys: [openKeys.pop()].filter(item => item) as string[] });
+    } else {
+      this.setState({ openKeys: [...openKeys] });
+    }
   };
 
   render() {
@@ -84,13 +90,13 @@ export default class SiderMenu extends Component<SiderMenuProps, SiderMenuState>
     });
     return (
       <Sider
+        collapsible
         trigger={null}
-        collapsible={true}
         collapsed={collapsed}
         breakpoint="lg"
         onCollapse={collapse => {
           if (firstMount || !isMobile) {
-            onCollapse(collapse);
+            onCollapse!(collapse);
           }
         }}
         width={256}

+ 8 - 16
src/components/SiderMenu/SiderMenuUtils.ts

@@ -1,13 +1,14 @@
 import pathToRegexp from 'path-to-regexp';
 import { urlToList } from '../_utils/pathTools';
+import { MenuDataItem, BaseMenuProps } from './BaseMenu';
 
 /**
  * Recursively flatten the data
  * [{path:string},{path:string}] => {path,path2}
  * @param  menus
  */
-export const getFlatMenuKeys = menuData => {
-  let keys = [];
+export const getFlatMenuKeys = (menuData: MenuDataItem[] = []) => {
+  let keys: string[] = [];
   menuData.forEach(item => {
     keys.push(item.path);
     if (item.children) {
@@ -17,24 +18,15 @@ export const getFlatMenuKeys = menuData => {
   return keys;
 };
 
-export const getMenuMatches = (flatMenuKeys, path) =>
-  flatMenuKeys.filter(item => {
-    if (item) {
-      return pathToRegexp(item).test(path);
-    }
-    return false;
-  });
+export const getMenuMatches = (flatMenuKeys: string[] = [], path: string) =>
+  flatMenuKeys.filter(item => item && pathToRegexp(item).test(path));
 
 /**
  * 获得菜单子节点
- * @memberof SiderMenu
  */
-export const getDefaultCollapsedSubMenus = props => {
-  const {
-    location: { pathname },
-    flatMenuKeys,
-  } = props;
-  return urlToList(pathname)
+export const getDefaultCollapsedSubMenus = (props: BaseMenuProps) => {
+  const { location, flatMenuKeys } = props;
+  return urlToList(location!.pathname)
     .map(item => getMenuMatches(flatMenuKeys, item)[0])
     .filter(item => item)
     .reduce((acc, curr) => [...acc, curr], ['/']);

+ 9 - 12
src/components/SiderMenu/index.tsx

@@ -1,27 +1,20 @@
 import React from 'react';
 import { Drawer } from 'antd';
+import { SiderMenuProps } from './SiderMenu';
 import SiderMenu from './SiderMenu';
 import { getFlatMenuKeys } from './SiderMenuUtils';
 
-export declare type SiderTheme = 'light' | 'dark';
+export { SiderMenuProps };
+export { MenuDataItem } from './BaseMenu';
 
-interface SiderMenuProps {
-  isMobile: boolean;
-  menuData: any[];
-  collapsed: boolean;
-  logo?: string;
-  theme?: SiderTheme;
-  onCollapse: (payload: boolean) => void;
-}
-
-const SiderMenuWrapper: React.SFC<SiderMenuProps> = props => {
+const SiderMenuWrapper: React.FC<SiderMenuProps> = props => {
   const { isMobile, menuData, collapsed, onCollapse } = props;
   const flatMenuKeys = getFlatMenuKeys(menuData);
   return isMobile ? (
     <Drawer
       visible={!collapsed}
       placement="left"
-      onClose={() => onCollapse(true)}
+      onClose={() => onCollapse!(true)}
       style={{
         padding: 0,
         height: '100vh',
@@ -34,4 +27,8 @@ const SiderMenuWrapper: React.SFC<SiderMenuProps> = props => {
   );
 };
 
+SiderMenuWrapper.defaultProps = {
+  onCollapse: () => void 0,
+};
+
 export default React.memo(SiderMenuWrapper);

+ 14 - 47
src/components/TopNavHeader/index.tsx

@@ -1,58 +1,31 @@
+import { SiderMenuProps } from '@/components/SiderMenu';
 import React, { Component } from 'react';
 import Link from 'umi/link';
-import RightContent from '../GlobalHeader/RightContent';
+import RightContent, { GlobalHeaderRightProps } from '../GlobalHeader/RightContent';
 import BaseMenu from '../SiderMenu/BaseMenu';
 import { getFlatMenuKeys } from '../SiderMenu/SiderMenuUtils';
 import styles from './index.less';
-import defaultSettings from '../../../config/defaultSettings';
+import defaultSettings, { ContentWidth } from '../../../config/defaultSettings';
 
-export declare type CollapseType = 'clickTrigger' | 'responsive';
-export declare type SiderTheme = 'light' | 'dark';
-export declare type MenuMode =
-  | 'vertical'
-  | 'vertical-left'
-  | 'vertical-right'
-  | 'horizontal'
-  | 'inline';
-
-const { title } = defaultSettings;
-interface TopNavHeaderProps {
-  theme: SiderTheme;
-  contentWidth?: string;
-  menuData?: any[];
-  logo?: string;
-  mode?: MenuMode;
-  flatMenuKeys?: any[];
-  onCollapse?: (collapsed: boolean, type?: CollapseType) => void;
-  isMobile?: boolean;
-  openKeys?: any;
-  className?: string;
-  collapsed?: boolean;
-  handleOpenChange?: (openKeys: any[]) => void;
-  style?: React.CSSProperties;
-  onOpenChange?: (openKeys: string[]) => void;
-  onNoticeClear?: (type: string) => void;
-  onMenuClick?: ({ key: string }) => void;
-  onNoticeVisibleChange?: (b: boolean) => void;
+export interface TopNavHeaderProps extends SiderMenuProps, GlobalHeaderRightProps {
+  contentWidth?: ContentWidth;
 }
 
 interface TopNavHeaderState {
-  maxWidth: undefined | number;
+  maxWidth?: number;
 }
 
 export default class TopNavHeader extends Component<TopNavHeaderProps, TopNavHeaderState> {
-  state = {
-    maxWidth: undefined,
-  };
-
-  maim: HTMLDivElement;
-
-  static getDerivedStateFromProps(props) {
+  static getDerivedStateFromProps(props: TopNavHeaderProps) {
     return {
       maxWidth: (props.contentWidth === 'Fixed' ? 1200 : window.innerWidth) - 280 - 165 - 40,
     };
   }
 
+  state: TopNavHeaderState = {};
+
+  maim: HTMLDivElement | null = null;
+
   render() {
     const { theme, contentWidth, menuData, logo } = this.props;
     const { maxWidth } = this.state;
@@ -60,23 +33,17 @@ export default class TopNavHeader extends Component<TopNavHeaderProps, TopNavHea
     return (
       <div className={`${styles.head} ${theme === 'light' ? styles.light : ''}`}>
         <div
-          ref={ref => {
-            this.maim = ref;
-          }}
+          ref={ref => (this.maim = ref)}
           className={`${styles.main} ${contentWidth === 'Fixed' ? styles.wide : ''}`}
         >
           <div className={styles.left}>
             <div className={styles.logo} key="logo" id="logo">
               <Link to="/">
                 <img src={logo} alt="logo" />
-                <h1>{title}</h1>
+                <h1>{defaultSettings.title}</h1>
               </Link>
             </div>
-            <div
-              style={{
-                maxWidth,
-              }}
-            >
+            <div style={{ maxWidth }}>
               <BaseMenu {...this.props} flatMenuKeys={flatMenuKeys} className={styles.menu} />
             </div>
           </div>

+ 18 - 0
src/components/_utils/pathTools.test.ts

@@ -0,0 +1,18 @@
+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',
+    ]);
+  });
+});

+ 1 - 1
src/components/_utils/pathTools.ts

@@ -1,6 +1,6 @@
 // /userinfo/2144/id => ['/userinfo','/useinfo/2144,'/userindo/2144/id']
 // eslint-disable-next-line import/prefer-default-export
-export function urlToList(url) {
+export function urlToList(url: string) {
   const urllist = url.split('/').filter(i => i);
   return urllist.map((urlItem, index) => `/${urllist.slice(0, index + 1).join('/')}`);
 }

+ 6 - 5
src/global.tsx

@@ -14,7 +14,8 @@ if (pwa) {
   });
 
   // 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 () => {
       // Check if there is sw whose state is waiting in ServiceWorkerRegistration
       // https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerRegistration
@@ -25,11 +26,11 @@ if (pwa) {
       // Send skip-waiting event to waiting SW with MessageChannel
       await new Promise((resolve, reject) => {
         const channel = new MessageChannel();
-        channel.port1.onmessage = event => {
-          if (event.data.error) {
-            reject(event.data.error);
+        channel.port1.onmessage = msgEvent => {
+          if (msgEvent.data.error) {
+            reject(msgEvent.data.error);
           } else {
-            resolve(event.data);
+            resolve(msgEvent.data);
           }
         };
         worker.postMessage({ type: 'skip-waiting' }, [channel.port2]);

+ 28 - 31
src/layouts/BasicLayout.tsx

@@ -1,5 +1,6 @@
 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 { Layout } from 'antd';
 import classNames from 'classnames';
@@ -11,7 +12,7 @@ import useMedia from 'react-media-hook2';
 import logo from '../assets/logo.svg';
 import styles from './BasicLayout.less';
 import Footer from './Footer';
-import Header from './Header';
+import Header, { HeaderViewProps } from './Header';
 import Context from './MenuContext';
 
 // lazy load SettingDrawer
@@ -44,28 +45,21 @@ const query = {
   },
 };
 
-export declare type SiderTheme = 'light' | 'dark';
-
-interface BasicLayoutProps {
-  dispatch: (args: any) => void;
-  // wait for https://github.com/umijs/umi/pull/2036
-  route: any;
-  breadcrumbNameMap: object;
-  fixSiderbar: boolean;
-  layout: string;
-  navTheme: SiderTheme;
-  menuData: any[];
-  fixedHeader: boolean;
-  location: Location;
-  collapsed: boolean;
+export interface BasicLayoutProps
+  extends ConnectProps,
+    SiderMenuProps,
+    HeaderViewProps,
+    Partial<SettingModelState> {
+  breadcrumbNameMap: { [path: string]: MenuDataItem };
+  route: MenuDataItem;
 }
 
-interface BasicLayoutContext {
+export interface BasicLayoutContext {
   location: Location;
-  breadcrumbNameMap: object;
+  breadcrumbNameMap: { [path: string]: MenuDataItem };
 }
 
-const BasicLayout: React.SFC<BasicLayoutProps> = props => {
+const BasicLayout: React.FC<BasicLayoutProps> = props => {
   const {
     breadcrumbNameMap,
     dispatch,
@@ -79,18 +73,21 @@ const BasicLayout: React.SFC<BasicLayoutProps> = props => {
     navTheme,
     route: { routes, authority },
   } = props;
+  /**
+   * constructor
+   */
   useState(() => {
-    dispatch({ type: 'user/fetchCurrent' });
-    dispatch({ type: 'setting/getSetting' });
-    dispatch({ type: 'menu/getMenuData', payload: { routes, authority } });
+    dispatch!({ type: 'user/fetchCurrent' });
+    dispatch!({ type: 'setting/getSetting' });
+    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 hasLeftPadding = fixSiderbar && PropsLayout !== 'topmenu' && !isMobile;
-  const getContext = (): BasicLayoutContext => ({ location, breadcrumbNameMap });
   const handleMenuCollapse = (payload: boolean) =>
-    dispatch({ type: 'global/changeLayoutCollapsed', payload });
+    dispatch!({ type: 'global/changeLayoutCollapsed', payload });
   // Do not render SettingDrawer in production
   // unless it is deployed in preview.pro.ant.design as demo
   const renderSettingDrawer = () =>
@@ -98,7 +95,7 @@ const BasicLayout: React.SFC<BasicLayoutProps> = props => {
 
   const layout = (
     <Layout>
-      {isTop && !isMobile ? null : (
+      {PropsLayout === 'topmenu' && !isMobile ? null : (
         <SiderMenu
           logo={logo}
           theme={navTheme}
@@ -121,7 +118,7 @@ const BasicLayout: React.SFC<BasicLayoutProps> = props => {
           isMobile={isMobile}
           {...props}
         />
-        <Content className={styles.content} style={contentStyle}>
+        <Content className={styles.content} style={!fixedHeader ? { paddingTop: 0 } : {}}>
           {children}
         </Content>
         <Footer />
@@ -130,10 +127,10 @@ const BasicLayout: React.SFC<BasicLayoutProps> = props => {
   );
   return (
     <React.Fragment>
-      <DocumentTitle title={getPageTitle(location.pathname, breadcrumbNameMap)}>
+      <DocumentTitle title={getPageTitle(location!.pathname, breadcrumbNameMap)}>
         <ContainerQuery query={query}>
           {params => (
-            <Context.Provider value={getContext()}>
+            <Context.Provider value={{ location: location!, breadcrumbNameMap }}>
               <div className={classNames(params)}>{layout}</div>
             </Context.Provider>
           )}
@@ -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,
   layout: setting.layout,
   menuData: menuModel.menuData,

+ 2 - 6
src/layouts/BlankLayout.tsx

@@ -1,9 +1,5 @@
-import React, { ReactNode, SFC } from 'react';
+import React from 'react';
 
-interface BlankLayoutProps {
-  children: ReactNode;
-}
-
-const Layout: SFC<BlankLayoutProps> = ({ children }) => <div>{children}</div>;
+const Layout: React.FC = ({ children }) => <div>{children}</div>;
 
 export default Layout;

+ 29 - 29
src/layouts/Header.tsx

@@ -1,7 +1,8 @@
-import GlobalHeader from '@/components/GlobalHeader';
-import TopNavHeader from '@/components/TopNavHeader';
-import { DefaultSettings } from '../../config/defaultSettings';
+import GlobalHeader, { GlobalHeaderProps } from '@/components/GlobalHeader';
+import TopNavHeader, { TopNavHeaderProps } from '@/components/TopNavHeader';
+import { ConnectProps, ConnectState, SettingModelState } from '@/models/connect';
 import { Layout, message } from 'antd';
+import { ClickParam } from 'antd/es/menu';
 import { connect } from 'dva';
 import Animate from 'rc-animate';
 import React, { Component } from 'react';
@@ -11,15 +12,12 @@ import styles from './Header.less';
 
 const { Header } = Layout;
 
-export declare type SiderTheme = 'light' | 'dark';
-
-interface HeaderViewProps {
-  isMobile: boolean;
-  collapsed: boolean;
-  setting: DefaultSettings;
-  dispatch: (args: any) => void;
-  autoHideHeader: boolean;
-  handleMenuCollapse: (args: boolean) => void;
+export interface HeaderViewProps extends ConnectProps, TopNavHeaderProps, GlobalHeaderProps {
+  isMobile?: boolean;
+  collapsed?: boolean;
+  setting?: SettingModelState;
+  autoHideHeader?: boolean;
+  handleMenuCollapse?: (collapse: boolean) => void;
 }
 
 interface HeaderViewState {
@@ -27,6 +25,10 @@ interface HeaderViewState {
 }
 
 class HeaderView extends Component<HeaderViewProps, HeaderViewState> {
+  static defaultProps: HeaderViewProps = {
+    handleMenuCollapse: () => void 0,
+  };
+
   static getDerivedStateFromProps(props: HeaderViewProps, state: HeaderViewState) {
     if (!props.autoHideHeader && !state.visible) {
       return {
@@ -36,14 +38,12 @@ class HeaderView extends Component<HeaderViewProps, HeaderViewState> {
     return null;
   }
 
-  state = {
+  ticking: boolean = false;
+  oldScrollTop: number = 0;
+  state: HeaderViewState = {
     visible: true,
   };
 
-  ticking: boolean;
-
-  oldScrollTop: number;
-
   componentDidMount() {
     document.addEventListener('scroll', this.handScroll, { passive: true });
   }
@@ -54,27 +54,27 @@ class HeaderView extends Component<HeaderViewProps, HeaderViewState> {
 
   getHeadWidth = () => {
     const { isMobile, collapsed, setting } = this.props;
-    const { fixedHeader, layout } = setting;
+    const { fixedHeader, layout } = setting!;
     if (isMobile || !fixedHeader || layout === 'topmenu') {
       return '100%';
     }
     return collapsed ? 'calc(100% - 80px)' : 'calc(100% - 256px)';
   };
 
-  handleNoticeClear = type => {
+  handleNoticeClear = (type: string) => {
+    const { dispatch } = this.props;
     message.success(
       `${formatMessage({ id: 'component.noticeIcon.cleared' })} ${formatMessage({
         id: `component.globalHeader.${type}`,
-      })}`
+      })}`,
     );
-    const { dispatch } = this.props;
-    dispatch({
+    dispatch!({
       type: 'global/clearNotices',
       payload: type,
     });
   };
 
-  handleMenuClick = ({ key }) => {
+  handleMenuClick = ({ key }: ClickParam) => {
     const { dispatch } = this.props;
     if (key === 'userCenter') {
       router.push('/account/center');
@@ -89,16 +89,16 @@ class HeaderView extends Component<HeaderViewProps, HeaderViewState> {
       return;
     }
     if (key === 'logout') {
-      dispatch({
+      dispatch!({
         type: 'login/logout',
       });
     }
   };
 
-  handleNoticeVisibleChange = visible => {
+  handleNoticeVisibleChange = (visible: boolean) => {
     if (visible) {
       const { dispatch } = this.props;
-      dispatch({
+      dispatch!({
         type: 'global/fetchNotices',
       });
     }
@@ -135,7 +135,7 @@ class HeaderView extends Component<HeaderViewProps, HeaderViewState> {
 
   render() {
     const { isMobile, handleMenuCollapse, setting } = this.props;
-    const { navTheme, layout, fixedHeader } = setting;
+    const { navTheme, layout, fixedHeader } = setting!;
     const { visible } = this.state;
     const isTop = layout === 'topmenu';
     const width = this.getHeadWidth();
@@ -143,7 +143,7 @@ class HeaderView extends Component<HeaderViewProps, HeaderViewState> {
       <Header style={{ padding: 0, width }} className={fixedHeader ? styles.fixedHeader : ''}>
         {isTop && !isMobile ? (
           <TopNavHeader
-            theme={navTheme as SiderTheme}
+            theme={navTheme}
             mode="horizontal"
             onCollapse={handleMenuCollapse}
             onNoticeClear={this.handleNoticeClear}
@@ -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,
   collapsed: global.collapsed,
   fetchingMoreNotices: loading.effects['global/fetchMoreNotices'],

+ 9 - 13
src/layouts/UserLayout.tsx

@@ -1,4 +1,6 @@
 import SelectLang from '@/components/SelectLang';
+import { MenuDataItem } from '@/components/SiderMenu';
+import { ConnectProps, ConnectState } from '@/models/connect';
 import getPageTitle from '@/utils/getPageTitle';
 import { GlobalFooter } from 'ant-design-pro';
 import { Icon } from 'antd';
@@ -34,12 +36,10 @@ const copyright = (
   </Fragment>
 );
 
-interface UserLayoutProps {
-  dispatch: (args: any) => void;
-  route: any;
-  breadcrumbNameMap: object;
+export interface UserLayoutProps extends ConnectProps {
+  route: MenuDataItem;
+  breadcrumbNameMap: { [path: string]: MenuDataItem };
   navTheme: string;
-  location: Location;
 }
 
 class UserLayout extends Component<UserLayoutProps> {
@@ -48,20 +48,16 @@ class UserLayout extends Component<UserLayoutProps> {
       dispatch,
       route: { routes, authority },
     } = this.props;
-    dispatch({
+    dispatch!({
       type: 'menu/getMenuData',
       payload: { routes, authority },
     });
   }
 
   render() {
-    const {
-      children,
-      location: { pathname },
-      breadcrumbNameMap,
-    } = this.props;
+    const { children, location, breadcrumbNameMap } = this.props;
     return (
-      <DocumentTitle title={getPageTitle(pathname, breadcrumbNameMap)}>
+      <DocumentTitle title={getPageTitle(location!.pathname, breadcrumbNameMap)}>
         <div className={styles.container}>
           <div className={styles.lang}>
             <SelectLang />
@@ -85,7 +81,7 @@ class UserLayout extends Component<UserLayoutProps> {
   }
 }
 
-export default connect(({ menu: menuModel }) => ({
+export default connect(({ menu: menuModel }: ConnectState) => ({
   menuData: menuModel.menuData,
   breadcrumbNameMap: menuModel.breadcrumbNameMap,
 }))(UserLayout);

+ 61 - 0
src/models/connect.d.ts

@@ -0,0 +1,61 @@
+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;

+ 20 - 12
src/models/global.ts

@@ -1,10 +1,18 @@
 import { queryNotices } from '@/services/user';
-import { Effect, Subscription } from 'dva';
+import { Subscription } from 'dva';
 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 {
   collapsed: boolean;
-  notices: any[];
+  notices: NoticeItem[];
 }
 
 export interface GlobalModelType {
@@ -16,9 +24,9 @@ export interface GlobalModelType {
     changeNoticeReadState: Effect;
   };
   reducers: {
-    changeLayoutCollapsed: Reducer<any>;
-    saveNotices: Reducer<any>;
-    saveClearedNotices: Reducer<any>;
+    changeLayoutCollapsed: Reducer<GlobalModelState>;
+    saveNotices: Reducer<GlobalModelState>;
+    saveClearedNotices: Reducer<GlobalModelState>;
   };
   subscriptions: { setup: Subscription };
 }
@@ -38,8 +46,8 @@ const GlobalModel: GlobalModelType = {
         type: 'saveNotices',
         payload: data,
       });
-      const unreadCount = yield select(
-        state => state.global.notices.filter(item => !item.read).length
+      const unreadCount: number = yield select(
+        state => state.global.notices.filter(item => !item.read).length,
       );
       yield put({
         type: 'user/changeNotifyCount',
@@ -54,9 +62,9 @@ const GlobalModel: GlobalModelType = {
         type: 'saveClearedNotices',
         payload,
       });
-      const count = yield select(state => state.global.notices.length);
-      const unreadCount = yield select(
-        state => state.global.notices.filter(item => !item.read).length
+      const count: number = yield select(state => state.global.notices.length);
+      const unreadCount: number = yield select(
+        state => state.global.notices.filter(item => !item.read).length,
       );
       yield put({
         type: 'user/changeNotifyCount',
@@ -67,14 +75,14 @@ const GlobalModel: GlobalModelType = {
       });
     },
     *changeNoticeReadState({ payload }, { put, select }) {
-      const notices = yield select(state =>
+      const notices: NoticeItem[] = yield select(state =>
         state.global.notices.map(item => {
           const notice = { ...item };
           if (notice.id === payload) {
             notice.read = true;
           }
           return notice;
-        })
+        }),
       );
       yield put({
         type: 'saveNotices',

+ 34 - 49
src/models/menu.ts

@@ -1,33 +1,29 @@
+import { MenuDataItem } from '@/components/SiderMenu';
 import Authorized from '@/utils/Authorized';
 import { Effect } from 'dva';
 import isEqual from 'lodash/isEqual';
 import memoizeOne from 'memoize-one';
 import { Reducer } from 'redux';
 import { formatMessage } from 'umi-plugin-locale';
+import { IRoute } from 'umi-types';
 import defaultSettings from '../../config/defaultSettings';
-const { menu } = defaultSettings;
-const { check } = Authorized;
 
 // 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
+    .filter(item => item.name && item.path)
     .map(item => {
-      if (!item.name || !item.path) {
-        return null;
-      }
-
-      let locale = 'menu';
-      if (parentName) {
-        locale = `${parentName}.${item.name}`;
-      } else {
-        locale = `menu.${item.name}`;
-      }
+      const locale = `${parentName || 'menu'}.${item.name!}`;
       // if enableMenuLocale use item.name,
       // close menu international
-      const name = menu.disableLocal
-        ? item.name
-        : formatMessage({ id: locale, defaultMessage: item.name });
-      const result = {
+      const name = defaultSettings.menu.disableLocal
+        ? item.name!
+        : formatMessage({ id: locale, defaultMessage: item.name! });
+      const result: MenuDataItem = {
         ...item,
         name,
         locale,
@@ -40,55 +36,43 @@ function formatter(data: any[], parentAuthority: string[], parentName: string):
       }
       delete result.routes;
       return result;
-    })
-    .filter(item => item);
+    });
 }
 
 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
  */
-const getSubMenu: (item: SubMenuItem) => any = item => {
-  // doc: add hideChildrenInMenu
-  if (item.children && !item.hideChildrenInMenu && item.children.some(child => child.name)) {
-    return {
-      ...item,
-      children: filterMenuData(item.children), // eslint-disable-line
-    };
+const getSubMenu: (item: MenuDataItem) => MenuDataItem = item => {
+  if (
+    Array.isArray(item.children) &&
+    !item.hideChildrenInMenu &&
+    item.children.some(child => (child.name ? true : false))
+  ) {
+    const children = filterMenuData(item.children);
+    if (children.length) return { ...item, children };
   }
-  return item;
+  return { ...item, children: void 0 };
 };
 
 /**
  * filter menuData
  */
-const filterMenuData: (menuData: SubMenuItem[]) => SubMenuItem[] = menuData => {
-  if (!menuData) {
-    return [];
-  }
+const filterMenuData = (menuData: MenuDataItem[] = []): MenuDataItem[] => {
   return menuData
     .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);
 };
+
 /**
  * 获取面包屑映射
- * @param ISubMenuItem[] menuData 菜单配置
+ * @param MenuDataItem[] menuData 菜单配置
  */
-const getBreadcrumbNameMap: (menuData: SubMenuItem[]) => object = menuData => {
-  const routerMap = {};
-
-  const flattenMenuData: (data: SubMenuItem[]) => void = data => {
+const getBreadcrumbNameMap = (menuData: MenuDataItem[]) => {
+  const routerMap: { [key: string]: MenuDataItem } = {};
+  const flattenMenuData: (data: MenuDataItem[]) => void = data => {
     data.forEach(menuItem => {
       if (menuItem.children) {
         flattenMenuData(menuItem.children);
@@ -104,8 +88,8 @@ const getBreadcrumbNameMap: (menuData: SubMenuItem[]) => object = menuData => {
 const memoizeOneGetBreadcrumbNameMap = memoizeOne(getBreadcrumbNameMap, isEqual);
 
 export interface MenuModelState {
-  menuData: any[];
-  routerData: any[];
+  menuData: MenuDataItem[];
+  routerData: IRoute[];
   breadcrumbNameMap: object;
 }
 
@@ -116,9 +100,10 @@ export interface MenuModelType {
     getMenuData: Effect;
   };
   reducers: {
-    save: Reducer<any>;
+    save: Reducer<MenuModelState>;
   };
 }
+
 const MenuModel: MenuModelType = {
   namespace: 'menu',
 

+ 2 - 2
src/models/setting.ts

@@ -24,8 +24,8 @@ const updateTheme: (primaryColor?: string) => void = primaryColor => {
   const hideMessage = message.loading('正在编译主题!', 0);
   function buildIt() {
     if (!(window as any).less) {
-      console.log('no less');
-      return;
+      // tslint:disable-next-line no-console
+      return console.log('no less');
     }
     setTimeout(() => {
       (window as any).less

+ 12 - 10
src/models/user.ts

@@ -2,18 +2,20 @@ import { query as queryUsers, queryCurrent } from '@/services/user';
 import { Effect } from 'dva';
 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 {
   list: any[];
-  currentUser: {
-    avatar?: string;
-    name?: string;
-    title?: string;
-    group?: string;
-    signature?: string;
-    geographic?: any;
-    tags?: any[];
-    unreadCount?: number;
-  };
+  currentUser: CurrentUser;
 }
 
 export interface UserModelType {

+ 22 - 21
src/pages/Authorized.tsx

@@ -1,37 +1,38 @@
 import Authorized from '@/utils/Authorized';
+import { ConnectProps, ConnectState, UserModelState } from '@/models/connect';
 import { connect } from 'dva';
 import pathToRegexp from 'path-to-regexp';
 import React from 'react';
 import Redirect from 'umi/redirect';
-import { UserModelState } from '../models/user';
+import { IRoute } from 'umi-types';
 
-interface AuthComponentProps {
+interface AuthComponentProps extends ConnectProps {
   location: Location;
-  routerData: any[];
+  routerData: IRoute[];
   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 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 (
     <Authorized
-      authority={getRouteAuthority(location.pathname, routerData)}
+      authority={getRouteAuthority(location.pathname, routerData)!}
       noMatch={isLogin ? <Redirect to="/exception/403" /> : <Redirect to="/user/login" />}
     >
       {children}
@@ -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,
   user,
 }))(AuthComponent);

+ 4 - 4
src/service-worker.js

@@ -38,11 +38,11 @@ workbox.routing.registerRoute(/\/api\//, workbox.strategies.networkFirst());
  */
 workbox.routing.registerRoute(
   /^https:\/\/gw.alipayobjects.com\//,
-  workbox.strategies.networkFirst()
+  workbox.strategies.networkFirst(),
 );
 workbox.routing.registerRoute(
   /^https:\/\/cdnjs.cloudflare.com\//,
-  workbox.strategies.networkFirst()
+  workbox.strategies.networkFirst(),
 );
 workbox.routing.registerRoute(/\/color.less/, workbox.strategies.networkFirst());
 
@@ -58,8 +58,8 @@ addEventListener('message', event => {
         .skipWaiting()
         .then(
           () => replyPort.postMessage({ error: null }),
-          error => replyPort.postMessage({ error })
-        )
+          error => replyPort.postMessage({ error }),
+        ),
     );
   }
 });

+ 29 - 0
src/typings.d.ts

@@ -9,4 +9,33 @@ declare module '*.jpeg';
 declare module '*.gif';
 declare module '*.bmp';
 declare module '*.tiff';
+declare module 'rc-animate';
+declare module 'omit.js';
+declare module 'react-copy-to-clipboard';
 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';
+}

+ 1 - 1
src/utils/authority.test.ts

@@ -3,7 +3,7 @@ import { getAuthority } from './authority';
 
 describe('getAuthority should be strong', () => {
   it('empty', () => {
-    expect(getAuthority(null)).toEqual(['admin']); // default value
+    expect(getAuthority(null!)).toEqual(['admin']); // default value
   });
   it('string', () => {
     expect(getAuthority('admin')).toEqual(['admin']);

+ 1 - 1
src/utils/authority.ts

@@ -6,7 +6,7 @@ export function getAuthority(str?: string): any {
   // authorityString could be admin, "admin", ["admin"]
   let authority;
   try {
-    authority = JSON.parse(authorityString);
+    authority = JSON.parse(authorityString!);
   } catch (e) {
     authority = authorityString;
   }

+ 11 - 13
src/utils/getPageTitle.ts

@@ -3,24 +3,22 @@ import memoizeOne from 'memoize-one';
 import pathToRegexp from 'path-to-regexp';
 import { formatMessage } from 'umi-plugin-locale';
 import defaultSettings from '../../config/defaultSettings';
+import { MenuDataItem } from '@/components/SiderMenu/BaseMenu';
 
 const { menu, title } = defaultSettings;
 
-interface RouterData {
-  name: string;
-  locale: string;
-  authority?: string[];
-  children?: any[];
-  icon?: string;
-  path: string;
-}
-
-export const matchParamsPath = (pathname: string, breadcrumbNameMap: object): RouterData => {
+export const matchParamsPath = (
+  pathname: string,
+  breadcrumbNameMap: { [path: string]: MenuDataItem },
+): MenuDataItem => {
   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);
   if (!currRouterData) {
     return title;
@@ -28,7 +26,7 @@ const getPageTitle = (pathname: string, breadcrumbNameMap: object): string => {
   const pageName = menu.disableLocal
     ? currRouterData.name
     : formatMessage({
-        id: currRouterData.locale || currRouterData.name,
+        id: currRouterData.locale || currRouterData.name!,
         defaultMessage: currRouterData.name,
       });
 

+ 8 - 2
src/utils/request.ts

@@ -5,6 +5,12 @@
 import { extend } from 'umi-request';
 import { notification } from 'antd';
 
+interface ResponseError<D = any> extends Error {
+  name: string;
+  data: D;
+  response: Response;
+}
+
 const codeMessage = {
   200: '服务器成功返回请求的数据。',
   201: '新建或修改数据成功。',
@@ -26,8 +32,8 @@ const codeMessage = {
 /**
  * 异常处理程序
  */
-const errorHandler = error => {
-  const { response = {} } = error;
+const errorHandler = (error: ResponseError) => {
+  const { response = {} as Response } = error;
   const errortext = codeMessage[response.status] || response.statusText;
   const { status, url } = response;
 

+ 38 - 0
src/utils/utils.test.ts

@@ -0,0 +1,38 @@
+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();
+  });
+});

+ 1 - 1
src/utils/utils.ts

@@ -1,6 +1,6 @@
 /* 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]*))?)$/;
 
-export function isUrl(path) {
+export function isUrl(path: string) {
   return reg.test(path);
 }

+ 3 - 3
tsconfig.json

@@ -2,8 +2,8 @@
   "compilerOptions": {
     "outDir": "build/dist",
     "module": "esnext",
-    "target": "es2016",
-    "lib": ["es6", "dom"],
+    "target": "esnext",
+    "lib": ["esnext", "dom"],
     "sourceMap": true,
     "baseUrl": ".",
     "jsx": "react",
@@ -16,7 +16,7 @@
     "noUnusedLocals": true,
     "allowJs": true,
     "experimentalDecorators": true,
-    "strictNullChecks": true,
+    "strict": true,
     "paths": {
       "@/*": ["./src/*"]
     }

+ 0 - 13
tslint.json

@@ -1,13 +0,0 @@
-{
-  "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
-  }
-}

+ 95 - 0
tslint.yml

@@ -0,0 +1,95 @@
+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

+ 3 - 0
typings.d.ts

@@ -0,0 +1,3 @@
+declare module 'slash2';
+declare module 'antd-pro-merge-less';
+declare module 'antd-theme-webpack-plugin';