diff --git a/config/config.ts b/config/config.ts index 2f16ac4d..3f70e9df 100644 --- a/config/config.ts +++ b/config/config.ts @@ -1,5 +1,6 @@ import { IConfig, IPlugin } from 'umi-types'; import defaultSettings from './defaultSettings'; // https://umijs.org/config/ + import slash from 'slash2'; import themePluginConfig from './themePluginConfig'; import proxy from './proxy'; @@ -11,7 +12,6 @@ const { pwa } = defaultSettings; // preview.pro.ant.design 专用环境变量,请不要在你的项目中使用它。 const { ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION, REACT_APP_ENV } = process.env; const isAntDesignProPreview = ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION === 'site'; - const plugins: IPlugin[] = [ ['umi-plugin-antd-icon-config', {}], [ @@ -126,6 +126,12 @@ export default { }, ], }, + { + name: 'list.table-list', + icon: 'table', + path: '/list', + component: './ListTableList', + }, { component: './404', }, @@ -136,7 +142,6 @@ export default { }, ], }, - { component: './404', }, diff --git a/mock/user.ts b/mock/user.ts index 80cbd911..24fa3f7f 100644 --- a/mock/user.ts +++ b/mock/user.ts @@ -95,6 +95,15 @@ export default { }); return; } + if (type === 'mobile') { + res.send({ + status: 'ok', + type, + currentAuthority: 'admin', + }); + return; + } + res.send({ status: 'error', type, diff --git a/package.json b/package.json index 968aefd8..e1cf69d7 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "docker:dev": "docker-compose -f ./docker/docker-compose.dev.yml up", "docker:push": "npm run docker-hub:build && npm run docker:tag && docker push antdesign/ant-design-pro", "docker:tag": "docker tag ant-design-pro antdesign/ant-design-pro", - "fetch:blocks": "pro fetch-blocks && npm run prettier", + "fetch:blocks": "pro fetch-blocks --branch antd@4 && npm run prettier", "functions:build": "netlify-lambda build ./lambda", "functions:run": "cross-env NODE_ENV=dev netlify-lambda serve ./lambda", "gh-pages": "cp CNAME ./dist/ && gh-pages -d dist", @@ -33,7 +33,7 @@ "start:dev": "cross-env REACT_APP_ENV=dev MOCK=none umi dev", "start:no-mock": "cross-env MOCK=none umi dev", "start:no-ui": "cross-env UMI_UI=none umi dev", - "start:pre": "cross-env REACT_APP_ENV=pre MOCK=none umi dev", + "start:pre": "cross-env REACT_APP_ENV=pre umi dev", "start:test": "cross-env REACT_APP_ENV=test MOCK=none umi dev", "test": "umi test", "test:all": "node ./tests/run-tests.js", @@ -59,9 +59,11 @@ "not ie <= 10" ], "dependencies": { - "@ant-design/pro-layout": "^4.10.13", - "@antv/data-set": "^0.11.0", - "antd": "^3.23.6", + "@ant-design/icons": "^4.0.0-alpha.19", + "@ant-design/pro-layout": "^5.0.0", + "@ant-design/pro-table": "^2.0.0", + "@antv/data-set": "^0.10.2", + "antd": "^4.0.0-rc.1", "classnames": "^2.2.6", "dva": "^2.6.0-beta.16", "lodash": "^4.17.11", @@ -75,10 +77,12 @@ "react-helmet": "^5.2.1", "redux": "^4.0.1", "umi": "^2.13.0", + "umi-plugin-antd-icon-config": "^1.0.2", "umi-plugin-antd-theme": "^1.0.1", "umi-plugin-pro-block": "^1.3.2", - "umi-plugin-react": "^1.9.5", - "umi-request": "^1.0.8" + "umi-plugin-react": "^1.14.10", + "umi-request": "^1.0.8", + "use-merge-value": "^1.0.1" }, "devDependencies": { "@ant-design/pro-cli": "^1.0.18", @@ -88,14 +92,14 @@ "@types/jest": "^25.1.0", "@types/lodash": "^4.14.144", "@types/qs": "^6.5.3", - "@types/react": "^16.8.19", + "@types/react": "^16.9.17", "@types/react-dom": "^16.8.4", "@types/react-helmet": "^5.0.13", "@umijs/fabric": "^2.0.2", "chalk": "^3.0.0", "cross-env": "^7.0.0", "cross-port-killer": "^1.1.1", - "enzyme": "^3.9.0", + "enzyme": "^3.11.0", "express": "^4.17.1", "gh-pages": "^2.0.1", "husky": "^4.0.7", @@ -112,7 +116,7 @@ "umi-plugin-antd-icon-config": "^1.0.2", "umi-plugin-ga": "^1.1.3", "umi-plugin-pro": "^1.0.2", - "umi-types": "^0.5.0" + "umi-types": "^0.5.9" }, "optionalDependencies": { "puppeteer": "^2.0.0" diff --git a/src/components/GlobalHeader/AvatarDropdown.tsx b/src/components/GlobalHeader/AvatarDropdown.tsx index bcc2ffae..a6fbe0f2 100644 --- a/src/components/GlobalHeader/AvatarDropdown.tsx +++ b/src/components/GlobalHeader/AvatarDropdown.tsx @@ -1,10 +1,9 @@ -import { Avatar, Icon, Menu, Spin } from 'antd'; +import { LogoutOutlined, SettingOutlined, UserOutlined } from '@ant-design/icons'; +import { Avatar, Menu, Spin } from 'antd'; import { ClickParam } from 'antd/es/menu'; -import { FormattedMessage } from 'umi-plugin-react/locale'; import React from 'react'; import { connect } from 'dva'; import { router } from 'umi'; - import { ConnectProps, ConnectState } from '@/models/connect'; import { CurrentUser } from '@/models/user'; import HeaderDropdown from '../HeaderDropdown'; @@ -21,6 +20,7 @@ class AvatarDropdown extends React.Component { if (key === 'logout') { const { dispatch } = this.props; + if (dispatch) { dispatch({ type: 'login/logout', @@ -29,35 +29,40 @@ class AvatarDropdown extends React.Component { return; } + router.push(`/account/${key}`); }; render(): React.ReactNode { - const { currentUser = { avatar: '', name: '' }, menu } = this.props; - + const { + currentUser = { + avatar: '', + name: '', + }, + menu, + } = this.props; const menuHeaderDropdown = ( {menu && ( - - + + 个人中心 )} {menu && ( - - + + 个人设置 )} {menu && } - - + + 退出登录 ); - return currentUser && currentUser.name ? ( @@ -66,10 +71,17 @@ class AvatarDropdown extends React.Component { ) : ( - + ); } } + export default connect(({ user }: ConnectState) => ({ currentUser: user.currentUser, }))(AvatarDropdown); diff --git a/src/components/GlobalHeader/NoticeIconView.tsx b/src/components/GlobalHeader/NoticeIconView.tsx index 0f019060..6fbdd112 100644 --- a/src/components/GlobalHeader/NoticeIconView.tsx +++ b/src/components/GlobalHeader/NoticeIconView.tsx @@ -1,14 +1,12 @@ import React, { Component } from 'react'; import { Tag, message } from 'antd'; import { connect } from 'dva'; -import { formatMessage } from 'umi-plugin-react/locale'; import groupBy from 'lodash/groupBy'; import moment from 'moment'; - import { NoticeItem } from '@/models/global'; -import NoticeIcon from '../NoticeIcon'; import { CurrentUser } from '@/models/user'; import { ConnectProps, ConnectState } from '@/models/connect'; +import NoticeIcon from '../NoticeIcon'; import styles from './index.less'; export interface GlobalHeaderRightProps extends ConnectProps { @@ -22,6 +20,7 @@ export interface GlobalHeaderRightProps extends ConnectProps { class GlobalHeaderRight extends Component { componentDidMount() { const { dispatch } = this.props; + if (dispatch) { dispatch({ type: 'global/fetchNotices', @@ -32,6 +31,7 @@ class GlobalHeaderRight extends Component { changeReadState = (clickedItem: NoticeItem): void => { const { id } = clickedItem; const { dispatch } = this.props; + if (dispatch) { dispatch({ type: 'global/changeNoticeReadState', @@ -42,7 +42,8 @@ class GlobalHeaderRight extends Component { handleNoticeClear = (title: string, key: string) => { const { dispatch } = this.props; - message.success(`${formatMessage({ id: 'component.noticeIcon.cleared' })} ${title}`); + message.success(`${'清空了'} ${title}`); + if (dispatch) { dispatch({ type: 'global/clearNotices', @@ -51,19 +52,26 @@ class GlobalHeaderRight extends Component { } }; - getNoticeData = (): { [key: string]: NoticeItem[] } => { + getNoticeData = (): { + [key: string]: NoticeItem[]; + } => { const { notices = [] } = this.props; + if (notices.length === 0) { return {}; } + const newNotices = notices.map(notice => { const newNotice = { ...notice }; + if (newNotice.datetime) { newNotice.datetime = moment(notice.datetime as string).fromNow(); } + if (newNotice.id) { newNotice.key = newNotice.id; } + if (newNotice.extra && newNotice.status) { const color = { todo: '', @@ -72,23 +80,33 @@ class GlobalHeaderRight extends Component { doing: 'gold', }[newNotice.status]; newNotice.extra = ( - + {newNotice.extra} ); } + return newNotice; }); return groupBy(newNotices, 'type'); }; getUnreadData = (noticeData: { [key: string]: NoticeItem[] }) => { - const unreadMsg: { [key: string]: number } = {}; + const unreadMsg: { + [key: string]: number; + } = {}; Object.keys(noticeData).forEach(key => { const value = noticeData[key]; + if (!unreadMsg[key]) { unreadMsg[key] = 0; } + if (Array.isArray(value)) { unreadMsg[key] = value.filter(item => !item.read).length; } @@ -100,7 +118,6 @@ class GlobalHeaderRight extends Component { const { currentUser, fetchingNotices, onNoticeVisibleChange } = this.props; const noticeData = this.getNoticeData(); const unreadMsg = this.getUnreadData(noticeData); - return ( { this.changeReadState(item as NoticeItem); }} loading={fetchingNotices} - clearText={formatMessage({ id: 'component.noticeIcon.clear' })} - viewMoreText={formatMessage({ id: 'component.noticeIcon.view-more' })} + clearText="清空" + viewMoreText="查看更多" onClear={this.handleNoticeClear} onPopupVisibleChange={onNoticeVisibleChange} onViewMore={() => message.info('Click on view more')} @@ -120,22 +137,22 @@ class GlobalHeaderRight extends Component { tabKey="notification" count={unreadMsg.notification} list={noticeData.notification} - title={formatMessage({ id: 'component.globalHeader.notification' })} - emptyText={formatMessage({ id: 'component.globalHeader.notification.empty' })} + title="通知" + emptyText="你已查看所有通知" showViewMore /> = props => {
umi ui, value: 'umi ui' }, + { + label: Ant Design, + value: 'Ant Design', + }, + { + label: Pro Table, + value: 'Pro Table', + }, + { + label: Pro Layout, + value: 'Pro Layout', + }, ]} - onSearch={() => {}} - onPressEnter={() => {}} + // onSearch={value => { + // //console.log('input', value); + // }} /> - + - + - {REACT_APP_ENV && {REACT_APP_ENV}} + {REACT_APP_ENV && ( + + {REACT_APP_ENV} + + )}
); diff --git a/src/components/GlobalHeader/index.less b/src/components/GlobalHeader/index.less index 8d6b641a..6a156dba 100644 --- a/src/components/GlobalHeader/index.less +++ b/src/components/GlobalHeader/index.less @@ -12,17 +12,19 @@ } .right { + display: flex; float: right; height: @layout-header-height; margin-left: auto; overflow: hidden; .action { - display: inline-block; + display: flex; + align-items: center; height: 100%; padding: 0 12px; cursor: pointer; transition: all 0.3s; - > i { + > span { color: @text-color; vertical-align: middle; } @@ -53,7 +55,7 @@ .dark { .action { color: rgba(255, 255, 255, 0.85); - > i { + > span { color: rgba(255, 255, 255, 0.85); } &:hover, @@ -67,12 +69,12 @@ .dark { .action { color: @text-color; - > i { + > span { color: @text-color; } &:hover { color: rgba(255, 255, 255, 0.85); - > i { + > span { color: rgba(255, 255, 255, 0.85); } } diff --git a/src/components/HeaderDropdown/index.tsx b/src/components/HeaderDropdown/index.tsx index f668a2b6..cc60727d 100644 --- a/src/components/HeaderDropdown/index.tsx +++ b/src/components/HeaderDropdown/index.tsx @@ -6,9 +6,9 @@ import styles from './index.less'; declare type OverlayFunc = () => React.ReactNode; -export interface HeaderDropdownProps extends DropDownProps { +export interface HeaderDropdownProps extends Omit { overlayClassName?: string; - overlay: React.ReactNode | OverlayFunc; + overlay: React.ReactNode | OverlayFunc | any; placement?: 'bottomLeft' | 'bottomRight' | 'topLeft' | 'topCenter' | 'topRight' | 'bottomCenter'; } diff --git a/src/components/HeaderSearch/index.less b/src/components/HeaderSearch/index.less index 8f40cc7f..9af69d5f 100644 --- a/src/components/HeaderSearch/index.less +++ b/src/components/HeaderSearch/index.less @@ -1,12 +1,10 @@ @import '~antd/es/style/themes/default.less'; .headerSearch { - :global(.anticon-search) { - font-size: 16px; - cursor: pointer; - } .input { width: 0; + min-width: 0; + overflow: hidden; background: transparent; border-radius: 0; transition: width 0.3s, margin-left 0.3s; diff --git a/src/components/HeaderSearch/index.tsx b/src/components/HeaderSearch/index.tsx index c545f842..e09cf5b7 100644 --- a/src/components/HeaderSearch/index.tsx +++ b/src/components/HeaderSearch/index.tsx @@ -1,147 +1,105 @@ -import { AutoComplete, Icon, Input } from 'antd'; -import { AutoCompleteProps, DataSourceItemType } from 'antd/es/auto-complete'; -import React, { Component } from 'react'; +import { SearchOutlined } from '@ant-design/icons'; +import { AutoComplete, Input } from 'antd'; +import useMergeValue from 'use-merge-value'; +import { AutoCompleteProps } from 'antd/es/auto-complete'; +import React, { useRef } from 'react'; import classNames from 'classnames'; -import debounce from 'lodash/debounce'; import styles from './index.less'; export interface HeaderSearchProps { - onPressEnter: (value: string) => void; - onSearch: (value: string) => void; - onChange: (value: string) => void; - onVisibleChange: (b: boolean) => void; - className: string; - placeholder: string; - defaultActiveFirstOption: boolean; - dataSource: DataSourceItemType[]; - defaultOpen: boolean; + onSearch?: (value?: string) => void; + onChange?: (value?: string) => void; + onVisibleChange?: (b: boolean) => void; + className?: string; + placeholder?: string; + options: AutoCompleteProps['options']; + defaultOpen?: boolean; open?: boolean; defaultValue?: string; -} - -interface HeaderSearchState { value?: string; - searchMode: boolean; } -export default class HeaderSearch extends Component { - private inputRef: Input | null = null; - - static defaultProps = { - defaultActiveFirstOption: false, - onPressEnter: () => {}, - onSearch: () => {}, - onChange: () => {}, - className: '', - placeholder: '', - dataSource: [], - defaultOpen: false, - onVisibleChange: () => {}, - }; - - static getDerivedStateFromProps(props: HeaderSearchProps) { - if ('open' in props) { - return { - searchMode: props.open, - }; - } - return null; - } - - constructor(props: HeaderSearchProps) { - super(props); - this.state = { - searchMode: props.defaultOpen, - value: props.defaultValue, - }; - this.debouncePressEnter = debounce(this.debouncePressEnter, 500, { - leading: true, - trailing: false, - }); - } +const HeaderSearch: React.FC = props => { + const { + className, + defaultValue, + onVisibleChange, + placeholder, + open, + defaultOpen, + ...restProps + } = props; - onKeyDown = (e: React.KeyboardEvent) => { - if (e.key === 'Enter') { - this.debouncePressEnter(); - } - }; + const inputRef = useRef(null); - onChange: AutoCompleteProps['onChange'] = value => { - if (typeof value === 'string') { - const { onSearch, onChange } = this.props; - this.setState({ value }); - if (onSearch) { - onSearch(value); - } - if (onChange) { - onChange(value); - } - } - }; + const [value, setValue] = useMergeValue(defaultValue, { + value: props.value, + onChange: props.onChange, + }); - enterSearchMode = () => { - const { onVisibleChange } = this.props; - onVisibleChange(true); - this.setState({ searchMode: true }, () => { - const { searchMode } = this.state; - if (searchMode && this.inputRef) { - this.inputRef.focus(); - } - }); - }; + const [searchMode, setSearchMode] = useMergeValue(defaultOpen || false, { + value: props.open, + onChange: onVisibleChange, + }); - leaveSearchMode = () => { - this.setState({ - searchMode: false, - }); - }; + const inputClass = classNames(styles.input, { + [styles.show]: searchMode, + }); - debouncePressEnter = () => { - const { onPressEnter } = this.props; - const { value } = this.state; - onPressEnter(value || ''); - }; - - render() { - const { className, defaultValue, placeholder, open, ...restProps } = this.props; - const { searchMode, value } = this.state; - delete restProps.defaultOpen; // for rc-select not affected - const inputClass = classNames(styles.input, { - [styles.show]: searchMode, - }); - - return ( - { - if (propertyName === 'width' && !searchMode) { - const { onVisibleChange } = this.props; + return ( +
{ + setSearchMode(true); + if (searchMode && inputRef.current) { + inputRef.current.focus(); + } + }} + onTransitionEnd={({ propertyName }) => { + if (propertyName === 'width' && !searchMode) { + if (onVisibleChange) { onVisibleChange(searchMode); } + } + }} + > + + - - - { - this.inputRef = node; - }} - defaultValue={defaultValue} - aria-label={placeholder} - placeholder={placeholder} - onKeyDown={this.onKeyDown} - onBlur={this.leaveSearchMode} - /> - - - ); - } -} + { + if (e.key === 'Enter') { + if (restProps.onSearch) { + restProps.onSearch(value); + } + } + }} + onBlur={() => { + setSearchMode(false); + }} + /> + +
+ ); +}; + +export default HeaderSearch; diff --git a/src/components/NoticeIcon/index.tsx b/src/components/NoticeIcon/index.tsx index 2b15c631..3c940948 100644 --- a/src/components/NoticeIcon/index.tsx +++ b/src/components/NoticeIcon/index.tsx @@ -1,5 +1,7 @@ -import { Badge, Icon, Spin, Tabs } from 'antd'; -import React, { Component } from 'react'; +import { BellOutlined } from '@ant-design/icons'; +import { Badge, Spin, Tabs } from 'antd'; +import useMergeValue from 'use-merge-value'; +import React from 'react'; import classNames from 'classnames'; import NoticeList, { NoticeIconTabProps } from './NoticeList'; @@ -34,57 +36,24 @@ export interface NoticeIconProps { clearText?: string; viewMoreText?: string; clearClose?: boolean; + emptyImage?: string; children: React.ReactElement[]; } -export default class NoticeIcon extends Component { - public static Tab: typeof NoticeList = NoticeList; - - static defaultProps = { - onItemClick: (): void => {}, - onPopupVisibleChange: (): void => {}, - onTabChange: (): void => {}, - onClear: (): void => {}, - onViewMore: (): void => {}, - loading: false, - clearClose: false, - emptyImage: 'https://gw.alipayobjects.com/zos/rmsportal/wAhyIChODzsoKIOBHcBk.svg', - }; - - state = { - visible: false, - }; - - onItemClick = (item: NoticeIconData, tabProps: NoticeIconTabProps): void => { - const { onItemClick } = this.props; - if (onItemClick) { - onItemClick(item, tabProps); - } - }; - - onClear = (name: string, key: string): void => { - const { onClear } = this.props; - if (onClear) { - onClear(name, key); - } - }; - - onTabChange = (tabType: string): void => { - const { onTabChange } = this.props; - if (onTabChange) { - onTabChange(tabType); - } - }; - - onViewMore = (tabProps: NoticeIconTabProps, event: MouseEvent): void => { - const { onViewMore } = this.props; - if (onViewMore) { - onViewMore(tabProps, event); - } - }; - - getNotificationBox(): React.ReactNode { - const { children, loading, clearText, viewMoreText } = this.props; +const NoticeIcon: React.FC & { + Tab: typeof NoticeList; +} = props => { + const getNotificationBox = (): React.ReactNode => { + const { + children, + loading, + onClear, + onTabChange, + onItemClick, + onViewMore, + clearText, + viewMoreText, + } = props; if (!children) { return null; } @@ -103,9 +72,9 @@ export default class NoticeIcon extends Component { clearText={clearText} viewMoreText={viewMoreText} data={list} - onClear={(): void => this.onClear(title, tabKey)} - onClick={(item): void => this.onItemClick(item, child.props)} - onViewMore={(event): void => this.onViewMore(child.props, event)} + onClear={(): void => onClear && onClear(title, tabKey)} + onClick={(item): void => onItemClick && onItemClick(item, child.props)} + onViewMore={(event): void => onViewMore && onViewMore(child.props, event)} showClear={showClear} showViewMore={showViewMore} title={title} @@ -115,59 +84,52 @@ export default class NoticeIcon extends Component { ); }); return ( - <> - - - {panes} - - - + + + {panes} + + ); - } - - handleVisibleChange = (visible: boolean): void => { - const { onPopupVisibleChange } = this.props; - this.setState({ visible }); - if (onPopupVisibleChange) { - onPopupVisibleChange(visible); - } }; - render(): React.ReactNode { - const { className, count, popupVisible, bell } = this.props; - const { visible } = this.state; - const noticeButtonClass = classNames(className, styles.noticeButton); - const notificationBox = this.getNotificationBox(); - const NoticeBellIcon = bell || ; - const trigger = ( - - - {NoticeBellIcon} - - - ); - if (!notificationBox) { - return trigger; - } - const popoverProps: { - visible?: boolean; - } = {}; - if ('popupVisible' in this.props) { - popoverProps.visible = popupVisible; - } + const { className, count, bell } = props; - return ( - - {trigger} - - ); + const [visible, setVisible] = useMergeValue(false, { + value: props.popupVisible, + onChange: props.onPopupVisibleChange, + }); + const noticeButtonClass = classNames(className, styles.noticeButton); + const notificationBox = getNotificationBox(); + const NoticeBellIcon = bell || ; + const trigger = ( + + + {NoticeBellIcon} + + + ); + if (!notificationBox) { + return trigger; } -} + + return ( + + {trigger} + + ); +}; + +NoticeIcon.defaultProps = { + emptyImage: 'https://gw.alipayobjects.com/zos/rmsportal/wAhyIChODzsoKIOBHcBk.svg', +}; + +NoticeIcon.Tab = NoticeList; + +export default NoticeIcon; diff --git a/src/components/SelectLang/index.less b/src/components/SelectLang/index.less index 7cb057ed..c0da9b4b 100644 --- a/src/components/SelectLang/index.less +++ b/src/components/SelectLang/index.less @@ -13,7 +13,7 @@ line-height: @layout-header-height; vertical-align: top; cursor: pointer; - > i { + > span { font-size: 16px !important; transform: none !important; svg { diff --git a/src/components/SelectLang/index.tsx b/src/components/SelectLang/index.tsx index d173fb6d..95df015e 100644 --- a/src/components/SelectLang/index.tsx +++ b/src/components/SelectLang/index.tsx @@ -1,6 +1,6 @@ -import { Icon, Menu } from 'antd'; -import { formatMessage, getLocale, setLocale } from 'umi-plugin-react/locale'; - +import { GlobalOutlined } from '@ant-design/icons'; +import { Menu } from 'antd'; +import { getLocale, setLocale } from 'umi-plugin-react/locale'; import { ClickParam } from 'antd/es/menu'; import React from 'react'; import classNames from 'classnames'; @@ -10,10 +10,13 @@ import styles from './index.less'; interface SelectLangProps { className?: string; } + const SelectLang: React.FC = props => { const { className } = props; const selectedLang = getLocale(); + const changeLang = ({ key }: ClickParam): void => setLocale(key); + const locales = ['zh-CN', 'zh-TW', 'en-US', 'pt-BR']; const languageLabels = { 'zh-CN': '简体中文', @@ -42,7 +45,7 @@ const SelectLang: React.FC = props => { return ( - + ); diff --git a/src/layouts/BasicLayout.tsx b/src/layouts/BasicLayout.tsx index f82e2afb..f87b57b1 100644 --- a/src/layouts/BasicLayout.tsx +++ b/src/layouts/BasicLayout.tsx @@ -3,20 +3,19 @@ * You can view component api by: * https://github.com/ant-design/ant-design-pro-layout */ - import ProLayout, { MenuDataItem, BasicLayoutProps as ProLayoutProps, Settings, DefaultFooter, } from '@ant-design/pro-layout'; +import { formatMessage } from 'umi-plugin-react/locale'; import React, { useEffect } from 'react'; import { Link } from 'umi'; import { Dispatch } from 'redux'; import { connect } from 'dva'; -import { Icon, Result, Button } from 'antd'; -import { formatMessage } from 'umi-plugin-react/locale'; - +import { GithubOutlined } from '@ant-design/icons'; +import { Result, Button } from 'antd'; import Authorized from '@/utils/Authorized'; import RightContent from '@/components/GlobalHeader/RightContent'; import { ConnectState } from '@/models/connect'; @@ -35,7 +34,6 @@ const noMatch = ( } /> ); - export interface BasicLayoutProps extends ProLayoutProps { breadcrumbNameMap: { [path: string]: MenuDataItem; @@ -51,16 +49,13 @@ export type BasicLayoutContext = { [K in 'location']: BasicLayoutProps[K] } & { [path: string]: MenuDataItem; }; }; - /** * use Authorized check all menu item */ + const menuDataRender = (menuList: MenuDataItem[]): MenuDataItem[] => menuList.map(item => { - const localItem = { - ...item, - children: item.children ? menuDataRender(item.children) : [], - }; + const localItem = { ...item, children: item.children ? menuDataRender(item.children) : [] }; return Authorized.check(item.authority, localItem, null) as MenuDataItem; }); @@ -76,7 +71,7 @@ const defaultFooterDom = ( }, { key: 'github', - title: , + title: , href: 'https://github.com/ant-design/ant-design-pro', blankTarget: true, }, @@ -94,6 +89,7 @@ const footerRender: BasicLayoutProps['footerRender'] = () => { if (!isAntDesignPro()) { return defaultFooterDom; } + return ( <> {defaultFooterDom} @@ -116,7 +112,14 @@ const footerRender: BasicLayoutProps['footerRender'] = () => { }; const BasicLayout: React.FC = props => { - const { dispatch, children, settings, location = { pathname: '/' } } = props; + const { + dispatch, + children, + settings, + location = { + pathname: '/', + }, + } = props; /** * constructor */ @@ -131,6 +134,7 @@ const BasicLayout: React.FC = props => { /** * init variables */ + const handleMenuCollapse = (payload: boolean): void => { if (dispatch) { dispatch({ @@ -138,15 +142,15 @@ const BasicLayout: React.FC = props => { payload, }); } - }; - // get children authority + }; // get children authority + const authorized = getAuthorityFromRouter(props.route.routes, location.pathname || '/') || { authority: undefined, }; - return ( ( {logoDom} @@ -158,15 +162,13 @@ const BasicLayout: React.FC = props => { if (menuItemProps.isUrl || menuItemProps.children || !menuItemProps.path) { return defaultDom; } + return {defaultDom}; }} breadcrumbRender={(routers = []) => [ { path: '/', - breadcrumbName: formatMessage({ - id: 'menu.home', - defaultMessage: 'Home', - }), + breadcrumbName: '首页', }, ...routers, ]} @@ -180,7 +182,6 @@ const BasicLayout: React.FC = props => { }} footerRender={footerRender} menuDataRender={menuDataRender} - formatMessage={formatMessage} rightContentRender={() => } {...props} {...settings} diff --git a/src/layouts/UserLayout.tsx b/src/layouts/UserLayout.tsx index ccde893d..add204e6 100644 --- a/src/layouts/UserLayout.tsx +++ b/src/layouts/UserLayout.tsx @@ -2,16 +2,17 @@ import { DefaultFooter, MenuDataItem, getMenuData, getPageTitle } from '@ant-des import { Helmet } from 'react-helmet'; import { Link } from 'umi'; import React from 'react'; -import { connect } from 'dva'; import { formatMessage } from 'umi-plugin-react/locale'; - +import { connect } from 'dva'; import SelectLang from '@/components/SelectLang'; import { ConnectProps, ConnectState } from '@/models/connect'; import logo from '../assets/logo.svg'; import styles from './UserLayout.less'; export interface UserLayoutProps extends ConnectProps { - breadcrumbNameMap: { [path: string]: MenuDataItem }; + breadcrumbNameMap: { + [path: string]: MenuDataItem; + }; } const UserLayout: React.FC = props => { @@ -30,8 +31,8 @@ const UserLayout: React.FC = props => { const { breadcrumb } = getMenuData(routes); const title = getPageTitle({ pathname: location.pathname, - breadcrumb, formatMessage, + breadcrumb, ...props, }); return ( @@ -63,6 +64,4 @@ const UserLayout: React.FC = props => { ); }; -export default connect(({ settings }: ConnectState) => ({ - ...settings, -}))(UserLayout); +export default connect(({ settings }: ConnectState) => ({ ...settings }))(UserLayout); diff --git a/src/models/login.ts b/src/models/login.ts index 1e37a05f..671a15a7 100644 --- a/src/models/login.ts +++ b/src/models/login.ts @@ -3,7 +3,7 @@ import { Effect } from 'dva'; import { stringify } from 'querystring'; import { router } from 'umi'; -import { fakeAccountLogin, getFakeCaptcha } from '@/services/login'; +import { fakeAccountLogin } from '@/services/login'; import { setAuthority } from '@/utils/authority'; import { getPageQuery } from '@/utils/utils'; @@ -18,7 +18,6 @@ export interface LoginModelType { state: StateType; effects: { login: Effect; - getCaptcha: Effect; logout: Effect; }; reducers: { @@ -61,10 +60,6 @@ const Model: LoginModelType = { } }, - *getCaptcha({ payload }, { call }) { - yield call(getFakeCaptcha, payload); - }, - logout() { const { redirect } = getPageQuery(); // Note: There may be security issues, please note diff --git a/src/pages/404.tsx b/src/pages/404.tsx index f0c19049..d0460741 100644 --- a/src/pages/404.tsx +++ b/src/pages/404.tsx @@ -2,9 +2,6 @@ import { Button, Result } from 'antd'; import React from 'react'; import { router } from 'umi'; -// 这里应该使用 antd 的 404 result 组件, -// 但是还没发布,先来个简单的。 - const NoFoundPage: React.FC<{}> = () => ( ( @@ -16,8 +17,7 @@ export default (): React.ReactNode => ( }} /> - Ant Design Pro{' '} - You + Ant Design Pro You

diff --git a/src/pages/ListTableList/_mock.ts b/src/pages/ListTableList/_mock.ts new file mode 100644 index 00000000..33b74a96 --- /dev/null +++ b/src/pages/ListTableList/_mock.ts @@ -0,0 +1,154 @@ +import { Request, Response } from 'express'; +import { parse } from 'url'; +import { TableListItem, TableListParams } from './data.d'; + +// mock tableListDataSource +const genList = (current: number, pageSize: number) => { + const tableListDataSource: TableListItem[] = []; + + for (let i = 0; i < pageSize; i += 1) { + const index = (current - 1) * 10 + i; + tableListDataSource.push({ + key: index, + disabled: i % 6 === 0, + href: 'https://ant.design', + avatar: [ + 'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png', + 'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png', + ][i % 2], + name: `TradeCode ${index}`, + owner: '曲丽丽', + desc: '这是一段描述', + callNo: Math.floor(Math.random() * 1000), + status: Math.floor(Math.random() * 10) % 4, + updatedAt: new Date(), + createdAt: new Date(), + progress: Math.ceil(Math.random() * 100), + }); + } + tableListDataSource.reverse(); + return tableListDataSource; +}; + +let tableListDataSource = genList(1, 100); + +function getRule(req: Request, res: Response, u: string) { + let url = u; + if (!url || Object.prototype.toString.call(url) !== '[object String]') { + // eslint-disable-next-line prefer-destructuring + url = req.url; + } + const { current = 1, pageSize = 10 } = req.query; + const params = (parse(url, true).query as unknown) as TableListParams; + + let dataSource = [...tableListDataSource].slice((current - 1) * pageSize, current * pageSize); + if (params.sorter) { + const s = params.sorter.split('_'); + dataSource = dataSource.sort((prev, next) => { + if (s[1] === 'descend') { + return next[s[0]] - prev[s[0]]; + } + return prev[s[0]] - next[s[0]]; + }); + } + + if (params.status) { + const status = params.status.split(','); + let filterDataSource: TableListItem[] = []; + status.forEach((s: string) => { + filterDataSource = filterDataSource.concat( + dataSource.filter(item => { + if (parseInt(`${item.status}`, 10) === parseInt(s.split('')[0], 10)) { + return true; + } + return false; + }), + ); + }); + dataSource = filterDataSource; + } + + if (params.name) { + dataSource = dataSource.filter(data => data.name.includes(params.name || '')); + } + const result = { + data: dataSource, + total: tableListDataSource.length, + success: true, + pageSize, + current: parseInt(`${params.currentPage}`, 10) || 1, + }; + + return res.json(result); +} + +function postRule(req: Request, res: Response, u: string, b: Request) { + let url = u; + if (!url || Object.prototype.toString.call(url) !== '[object String]') { + // eslint-disable-next-line prefer-destructuring + url = req.url; + } + + const body = (b && b.body) || req.body; + const { method, name, desc, key } = body; + + switch (method) { + /* eslint no-case-declarations:0 */ + case 'delete': + tableListDataSource = tableListDataSource.filter(item => key.indexOf(item.key) === -1); + break; + case 'post': + (() => { + const i = Math.ceil(Math.random() * 10000); + const newRule = { + key: tableListDataSource.length, + href: 'https://ant.design', + avatar: [ + 'https://gw.alipayobjects.com/zos/rmsportal/eeHMaZBwmTvLdIwMfBpg.png', + 'https://gw.alipayobjects.com/zos/rmsportal/udxAbMEhpwthVVcjLXik.png', + ][i % 2], + name, + owner: '曲丽丽', + desc, + callNo: Math.floor(Math.random() * 1000), + status: Math.floor(Math.random() * 10) % 2, + updatedAt: new Date(), + createdAt: new Date(), + progress: Math.ceil(Math.random() * 100), + }; + tableListDataSource.unshift(newRule); + return res.json(newRule); + })(); + return; + + case 'update': + (() => { + let newRule = {}; + tableListDataSource = tableListDataSource.map(item => { + if (item.key === key) { + newRule = { ...item, desc, name }; + return { ...item, desc, name }; + } + return item; + }); + return res.json(newRule); + })(); + return; + default: + break; + } + + const result = { + list: tableListDataSource, + pagination: { + total: tableListDataSource.length, + }, + }; + + res.json(result); +} + +export default { + 'GET /api/rule': getRule, + 'POST /api/rule': postRule, +}; diff --git a/src/pages/ListTableList/components/CreateForm.tsx b/src/pages/ListTableList/components/CreateForm.tsx new file mode 100644 index 00000000..817922a2 --- /dev/null +++ b/src/pages/ListTableList/components/CreateForm.tsx @@ -0,0 +1,25 @@ +import React from 'react'; +import { Modal } from 'antd'; + +interface CreateFormProps { + modalVisible: boolean; + onCancel: () => void; +} + +const CreateForm: React.FC = props => { + const { modalVisible, onCancel } = props; + + return ( + onCancel()} + footer={null} + > + {props.children} + + ); +}; + +export default CreateForm; diff --git a/src/pages/ListTableList/components/UpdateForm.tsx b/src/pages/ListTableList/components/UpdateForm.tsx new file mode 100644 index 00000000..2c20df39 --- /dev/null +++ b/src/pages/ListTableList/components/UpdateForm.tsx @@ -0,0 +1,215 @@ +import React, { useState } from 'react'; +import { Form, Button, DatePicker, Input, Modal, Radio, Select, Steps } from 'antd'; + +import { TableListItem } from '../data.d'; + +export interface FormValueType extends Partial { + target?: string; + template?: string; + type?: string; + time?: string; + frequency?: string; +} + +export interface UpdateFormProps { + onCancel: (flag?: boolean, formVals?: FormValueType) => void; + onSubmit: (values: FormValueType) => void; + updateModalVisible: boolean; + values: Partial; +} +const FormItem = Form.Item; +const { Step } = Steps; +const { TextArea } = Input; +const { Option } = Select; +const RadioGroup = Radio.Group; + +export interface UpdateFormState { + formVals: FormValueType; + currentStep: number; +} + +const formLayout = { + labelCol: { span: 7 }, + wrapperCol: { span: 13 }, +}; + +const UpdateForm: React.FC = props => { + const [formVals, setFormVals] = useState({ + name: props.values.name, + desc: props.values.desc, + key: props.values.key, + target: '0', + template: '0', + type: '1', + time: '', + frequency: 'month', + }); + + const [currentStep, setCurrentStep] = useState(0); + + const [form] = Form.useForm(); + + const { + onSubmit: handleUpdate, + onCancel: handleUpdateModalVisible, + updateModalVisible, + values, + } = props; + + const forward = () => setCurrentStep(currentStep + 1); + + const backward = () => setCurrentStep(currentStep - 1); + + const handleNext = async () => { + const fieldsValue = await form.validateFields(); + + setFormVals({ ...formVals, ...fieldsValue }); + + if (currentStep < 2) { + forward(); + } else { + handleUpdate(formVals); + } + }; + + const renderContent = () => { + if (currentStep === 1) { + return ( + <> + + + + + + + + + + + + + + ); + } + if (currentStep === 2) { + return ( + <> + + + + + + + + ); + } + return ( + <> + + + + +