From 0bc812b763084c647db9c8dc35fa3ae8140fd675 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BD=95=E4=B9=90?= Date: Thu, 14 Mar 2019 08:22:40 +0800 Subject: [PATCH] [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` --- .eslintrc.js | 2 + .prettierrc | 2 +- config/config.ts | 7 +- config/defaultSettings.ts | 53 ++++++--- package.json | 21 +++- src/app.ts | 13 ++- src/components/GlobalHeader/RightContent.tsx | 53 ++++----- src/components/GlobalHeader/index.tsx | 24 ++-- src/components/HeaderDropdown/index.tsx | 21 ++-- src/components/HeaderSearch/index.tsx | 29 ++--- src/components/PageLoading/index.tsx | 2 +- src/components/SelectLang/index.tsx | 7 +- .../SettingDrawer/BlockCheckbox.tsx | 5 +- src/components/SettingDrawer/ThemeColor.tsx | 87 +++++++-------- src/components/SettingDrawer/index.tsx | 62 +++++------ src/components/SiderMenu/BaseMenu.tsx | 103 ++++++++++-------- src/components/SiderMenu/SiderMenu.tsx | 54 +++++---- src/components/SiderMenu/SiderMenuUtils.ts | 24 ++-- src/components/SiderMenu/index.tsx | 21 ++-- src/components/TopNavHeader/index.tsx | 61 +++-------- src/components/_utils/pathTools.test.ts | 18 +++ src/components/_utils/pathTools.ts | 2 +- src/global.tsx | 11 +- src/layouts/BasicLayout.tsx | 59 +++++----- src/layouts/BlankLayout.tsx | 8 +- src/layouts/Header.tsx | 58 +++++----- src/layouts/UserLayout.tsx | 22 ++-- src/models/connect.d.ts | 61 +++++++++++ src/models/global.ts | 32 ++++-- src/models/menu.ts | 83 ++++++-------- src/models/setting.ts | 4 +- src/models/user.ts | 22 ++-- src/pages/Authorized.tsx | 43 ++++---- src/service-worker.js | 8 +- src/typings.d.ts | 29 +++++ src/utils/authority.test.ts | 2 +- src/utils/authority.ts | 2 +- src/utils/getPageTitle.ts | 24 ++-- src/utils/request.ts | 10 +- src/utils/utils.test.ts | 38 +++++++ src/utils/utils.ts | 2 +- tsconfig.json | 6 +- tslint.json | 13 --- tslint.yml | 95 ++++++++++++++++ typings.d.ts | 3 + 45 files changed, 758 insertions(+), 548 deletions(-) create mode 100644 src/components/_utils/pathTools.test.ts create mode 100644 src/models/connect.d.ts create mode 100644 src/utils/utils.test.ts delete mode 100644 tslint.json create mode 100644 tslint.yml create mode 100644 typings.d.ts diff --git a/.eslintrc.js b/.eslintrc.js index 10309cef..6e1deec0 100644 --- a/.eslintrc.js +++ b/.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'] } }, }, }; diff --git a/.prettierrc b/.prettierrc index 0cc0de65..d3698d4d 100644 --- a/.prettierrc +++ b/.prettierrc @@ -1,6 +1,6 @@ { "singleQuote": true, - "trailingComma": "es5", + "trailingComma": "all", "printWidth": 100, "overrides": [ { diff --git a/config/config.ts b/config/config.ts index ea5ba918..36b531e3 100644 --- a/config/config.ts +++ b/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; diff --git a/config/defaultSettings.ts b/config/defaultSettings.ts index 9aa47e02..d489b711 100644 --- a/config/defaultSettings.ts +++ b/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; diff --git a/package.json b/package.json index 82256900..9794764a 100644 --- a/package.json +++ b/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": { diff --git a/src/app.ts b/src/app.ts index 22236327..c2bb1b1b 100644 --- a/src/app.ts +++ b/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(); - } + }, ); } diff --git a/src/components/GlobalHeader/RightContent.tsx b/src/components/GlobalHeader/RightContent.tsx index 0d768987..69c61fae 100644 --- a/src/components/GlobalHeader/RightContent.tsx +++ b/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 { - getNoticeData() { + getNoticeData = (): { [key: string]: NoticeItem[] } => { const { notices = [] } = this.props; if (notices.length === 0) { return {}; @@ -41,7 +34,7 @@ export default class GlobalHeaderRight extends Component 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 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 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 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 }} /> @@ -152,23 +145,23 @@ export default class GlobalHeaderRight extends Component { - 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 > showViewMore /> - {currentUser.name ? ( + {currentUser && currentUser.name ? ( void; isMobile?: boolean; logo?: string; - onNoticeClear?: (type: string) => void; - onMenuClick?: ({ key: string }) => void; - onNoticeVisibleChange?: (b: boolean) => void; } export default class GlobalHeader extends Component { - 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() { diff --git a/src/components/HeaderDropdown/index.tsx b/src/components/HeaderDropdown/index.tsx index b311061b..1cb87040 100644 --- a/src/components/HeaderDropdown/index.tsx +++ b/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 { - render() { - const { overlayClassName, ...props } = this.props; +const HeaderDropdown: React.FC = ({ overlayClassName: cls, ...restProps }) => ( + +); - return ( - - ); - } -} +export default HeaderDropdown; diff --git a/src/components/HeaderSearch/index.tsx b/src/components/HeaderSearch/index.tsx index 34de1db7..521ef82f 100644 --- a/src/components/HeaderSearch/index.tsx +++ b/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 { static defaultProps = { defaultActiveFirstOption: false, @@ -37,7 +37,7 @@ export default class HeaderSearch extends Component {}, }; - 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 { + 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 { + onChange = (value: string) => { const { onSearch, onChange } = this.props; this.setState({ value }); if (onSearch) { @@ -86,8 +87,8 @@ export default class HeaderSearch extends Component { 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 { - this.input = node; + this.inputRef = node; }} aria-label={placeholder} placeholder={placeholder} diff --git a/src/components/PageLoading/index.tsx b/src/components/PageLoading/index.tsx index 24160279..0dafd7ad 100644 --- a/src/components/PageLoading/index.tsx +++ b/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 = () => (
diff --git a/src/components/SelectLang/index.tsx b/src/components/SelectLang/index.tsx index b145e39b..e3ae9975 100644 --- a/src/components/SelectLang/index.tsx +++ b/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 = props => { +const SelectLang: React.FC = 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': '简体中文', diff --git a/src/components/SettingDrawer/BlockCheckbox.tsx b/src/components/SettingDrawer/BlockCheckbox.tsx index 84800f89..5ad7acfb 100644 --- a/src/components/SettingDrawer/BlockCheckbox.tsx +++ b/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 = ({ value, onChange, list }) => ( + +const BlockChecbox: React.FC = ({ value, onChange, list }) => (
{list.map(item => ( diff --git a/src/components/SettingDrawer/ThemeColor.tsx b/src/components/SettingDrawer/ThemeColor.tsx index ac6cd2bb..a3fc061a 100644 --- a/src/components/SettingDrawer/ThemeColor.tsx +++ b/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 = ({ color, check, ...rest }) => ( -
+ +const Tag: React.FC = ({ color, check, ...rest }) => ( +
{check ? : ''}
); -interface ThemeColorProps { +export interface ThemeColorProps { colors?: any[]; title?: string; value: string; onChange: (color: string) => void; } -const ThemeColor: React.SFC = ({ 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 = ({ 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 (

{title}

diff --git a/src/components/SettingDrawer/index.tsx b/src/components/SettingDrawer/index.tsx index d4932d96..fb501ddd 100644 --- a/src/components/SettingDrawer/index.tsx +++ b/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 = ({ children, title, style }) => ( -
+const Body: React.FC = ({ children, title, style }) => ( +

{title}

{children}
); -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 { + collapse: boolean; +} + +@connect(({ setting }: ConnectState) => ({ setting })) class SettingDrawer extends Component { - 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 { ]; }; - 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 { } this.setState(nextState, () => { const { dispatch } = this.props; - dispatch({ + dispatch!({ type: 'setting/changeSetting', payload: this.state, }); @@ -124,7 +127,7 @@ class SettingDrawer extends Component { 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 { render() { const { setting } = this.props; - const { navTheme, primaryColor, layout, colorWeak } = setting; + const { navTheme, primaryColor, layout, colorWeak } = setting!; const { collapse } = this.state; return ( { placement="right" handler={
- +
} - style={{ - zIndex: 999, - }} + style={{ zIndex: 999 }} >
@@ -221,6 +216,7 @@ class SettingDrawer extends Component { this.changeSetting('colorWeak', checked)} diff --git a/src/components/SiderMenu/BaseMenu.tsx b/src/components/SiderMenu/BaseMenu.tsx index 991f03cf..31e7211d 100644 --- a/src/components/SiderMenu/BaseMenu.tsx +++ b/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: , -const getIcon = icon => { +const getIcon = (icon?: string | React.ReactNode) => { if (typeof icon === 'string') { if (isUrl(icon)) { return 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 { + authority?: string[] | string; + children?: MenuDataItem[]; + hideChildrenInMenu?: boolean; + hideInMenu?: boolean; + icon?: string; + locale?: string; + name?: string; + path: string; + routes?: R extends true ? MenuDataItem[] : 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 { + 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 { /** * 获得菜单子节点 - * @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 { }; // 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 ( { {name} ) : ( - name + item.name ) } key={item.path} @@ -110,7 +127,7 @@ export default class BaseMenu extends Component { * 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 { { - onCollapse(true); - } - : undefined - } + replace={itemPath === location!.pathname} + onClick={isMobile ? () => onCollapse!(true) : void 0} > {icon} {name} @@ -144,7 +155,7 @@ export default class BaseMenu extends Component { ); }; - conversionPath = path => { + conversionPath = (path: string) => { if (path && path.indexOf('http') === 0) { return path; } @@ -156,7 +167,7 @@ export default class BaseMenu extends Component { openKeys, theme, mode, - location: { pathname }, + location, className, collapsed, handleOpenChange, @@ -164,7 +175,7 @@ export default class BaseMenu extends Component { 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]]; } diff --git a/src/components/SiderMenu/SiderMenu.tsx b/src/components/SiderMenu/SiderMenu.tsx index 8a55f897..9325d1ae 100644 --- a/src/components/SiderMenu/SiderMenu.tsx +++ b/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 { - 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 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 }); }; - 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 }); return ( { if (firstMount || !isMobile) { - onCollapse(collapse); + onCollapse!(collapse); } }} width={256} diff --git a/src/components/SiderMenu/SiderMenuUtils.ts b/src/components/SiderMenu/SiderMenuUtils.ts index 0c398b88..14d24557 100644 --- a/src/components/SiderMenu/SiderMenuUtils.ts +++ b/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], ['/']); diff --git a/src/components/SiderMenu/index.tsx b/src/components/SiderMenu/index.tsx index 9716eb74..6f1de3e7 100644 --- a/src/components/SiderMenu/index.tsx +++ b/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 = props => { +const SiderMenuWrapper: React.FC = props => { const { isMobile, menuData, collapsed, onCollapse } = props; const flatMenuKeys = getFlatMenuKeys(menuData); return isMobile ? ( onCollapse(true)} + onClose={() => onCollapse!(true)} style={{ padding: 0, height: '100vh', @@ -34,4 +27,8 @@ const SiderMenuWrapper: React.SFC = props => { ); }; +SiderMenuWrapper.defaultProps = { + onCollapse: () => void 0, +}; + export default React.memo(SiderMenuWrapper); diff --git a/src/components/TopNavHeader/index.tsx b/src/components/TopNavHeader/index.tsx index a1b610b3..df0db03a 100644 --- a/src/components/TopNavHeader/index.tsx +++ b/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 { - 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
{ - this.maim = ref; - }} + ref={ref => (this.maim = ref)} className={`${styles.main} ${contentWidth === 'Fixed' ? styles.wide : ''}`} >
-
+
diff --git a/src/components/_utils/pathTools.test.ts b/src/components/_utils/pathTools.test.ts new file mode 100644 index 00000000..16b77382 --- /dev/null +++ b/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', + ]); + }); +}); diff --git a/src/components/_utils/pathTools.ts b/src/components/_utils/pathTools.ts index bfb94e74..739e4328 100644 --- a/src/components/_utils/pathTools.ts +++ b/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('/')}`); } diff --git a/src/global.tsx b/src/global.tsx index 65e9b0db..fc2a7359 100644 --- a/src/global.tsx +++ b/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]); diff --git a/src/layouts/BasicLayout.tsx b/src/layouts/BasicLayout.tsx index 4fc2a458..f164d0fe 100644 --- a/src/layouts/BasicLayout.tsx +++ b/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 { + breadcrumbNameMap: { [path: string]: MenuDataItem }; + route: MenuDataItem; } -interface BasicLayoutContext { +export interface BasicLayoutContext { location: Location; - breadcrumbNameMap: object; + breadcrumbNameMap: { [path: string]: MenuDataItem }; } -const BasicLayout: React.SFC = props => { +const BasicLayout: React.FC = props => { const { breadcrumbNameMap, dispatch, @@ -79,18 +73,21 @@ const BasicLayout: React.SFC = 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 = props => { const layout = ( - {isTop && !isMobile ? null : ( + {PropsLayout === 'topmenu' && !isMobile ? null : ( = props => { isMobile={isMobile} {...props} /> - + {children}