From 56a5bcf5f3f5f29bbee4dfda3c4efcf1e4754326 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E5=B8=85?= Date: Tue, 9 Apr 2019 19:50:22 +0800 Subject: [PATCH] use layout components --- package.json | 1 - src/components/GlobalHeader/RightContent.tsx | 72 ++++---- src/components/NoticeIcon/NoticeList.less | 105 ++++++++++++ src/components/NoticeIcon/NoticeList.tsx | 113 +++++++++++++ src/components/NoticeIcon/index.less | 31 ++++ src/components/NoticeIcon/index.tsx | 168 +++++++++++++++++++ src/layouts/BasicLayout.tsx | 2 + 7 files changed, 460 insertions(+), 32 deletions(-) create mode 100644 src/components/NoticeIcon/NoticeList.less create mode 100644 src/components/NoticeIcon/NoticeList.tsx create mode 100644 src/components/NoticeIcon/index.less create mode 100644 src/components/NoticeIcon/index.tsx diff --git a/package.json b/package.json index 00fdc109..c2a9cccb 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,6 @@ ], "dependencies": { "@ant-design/pro-layout": "^4.0.3", - "ant-design-pro": "^2.3.0", "antd": "^3.15.0", "classnames": "^2.2.6", "dva": "^2.4.0", diff --git a/src/components/GlobalHeader/RightContent.tsx b/src/components/GlobalHeader/RightContent.tsx index 91ea3093..f2ca00f9 100644 --- a/src/components/GlobalHeader/RightContent.tsx +++ b/src/components/GlobalHeader/RightContent.tsx @@ -1,4 +1,4 @@ -import { ConnectProps } from '@/models/connect'; +import { ConnectProps, ConnectState } from '@/models/connect'; import { NoticeItem } from '@/models/global'; import { CurrentUser } from '@/models/user'; import React, { Component } from 'react'; @@ -7,11 +7,12 @@ import { ClickParam } from 'antd/es/menu'; import { FormattedMessage, formatMessage } from 'umi-plugin-react/locale'; import moment from 'moment'; import groupBy from 'lodash/groupBy'; -import { NoticeIcon } from 'ant-design-pro'; +import NoticeIcon from '../NoticeIcon'; import HeaderSearch from '../HeaderSearch'; import HeaderDropdown from '../HeaderDropdown'; import SelectLang from '../SelectLang'; import styles from './index.less'; +import { connect } from 'dva'; export type SiderTheme = 'light' | 'dark'; @@ -21,11 +22,11 @@ export interface GlobalHeaderRightProps extends ConnectProps { fetchingNotices?: boolean; onNoticeVisibleChange?: (visible: boolean) => void; onMenuClick?: (param: ClickParam) => void; - onNoticeClear?: (tabName: string) => void; + onNoticeClear?: (tabName?: string) => void; theme?: SiderTheme; } -export default class GlobalHeaderRight extends Component { +class GlobalHeaderRight extends Component { getNoticeData = (): { [key: string]: NoticeItem[] } => { const { notices = [] } = this.props; if (notices.length === 0) { @@ -78,16 +79,24 @@ export default class GlobalHeaderRight extends Component payload: id, }); }; - + componentDidMount() { + const { dispatch } = this.props; + dispatch!({ + type: 'global/fetchNotices', + }); + } + handleNoticeClear = (title: string, key: string) => { + const { dispatch } = this.props; + message.success(`${formatMessage({ id: 'component.noticeIcon.cleared' })} ${title}`); + if (dispatch) { + dispatch({ + type: 'global/clearNotices', + payload: key, + }); + } + }; render() { - const { - currentUser, - fetchingNotices, - onNoticeVisibleChange, - onMenuClick, - onNoticeClear, - theme, - } = this.props; + const { currentUser, fetchingNotices, onNoticeVisibleChange, onMenuClick, theme } = this.props; const menu = ( @@ -146,46 +155,39 @@ export default class GlobalHeaderRight extends Component { - console.log(item, tabProps); // tslint:disable-line no-console + onItemClick={item => { 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' }), - notification: formatMessage({ id: 'component.globalHeader.notification' }), - message: formatMessage({ id: 'component.globalHeader.message' }), - event: formatMessage({ id: 'component.globalHeader.event' }), - }} - onClear={onNoticeClear} + clearText={formatMessage({ id: 'component.noticeIcon.clear' })} + viewMoreText={formatMessage({ id: 'component.noticeIcon.view-more' })} + onClear={this.handleNoticeClear} onPopupVisibleChange={onNoticeVisibleChange} onViewMore={() => message.info('Click on view more')} clearClose > @@ -209,3 +211,11 @@ export default class GlobalHeaderRight extends Component ); } } + +export default connect(({ user, global, loading }: ConnectState) => ({ + currentUser: user.currentUser, + collapsed: global.collapsed, + fetchingMoreNotices: loading.effects['global/fetchMoreNotices'], + fetchingNotices: loading.effects['global/fetchNotices'], + notices: global.notices, +}))(GlobalHeaderRight); diff --git a/src/components/NoticeIcon/NoticeList.less b/src/components/NoticeIcon/NoticeList.less new file mode 100644 index 00000000..efa686cb --- /dev/null +++ b/src/components/NoticeIcon/NoticeList.less @@ -0,0 +1,105 @@ +@import '~antd/lib/style/themes/default.less'; + +.list { + max-height: 400px; + overflow: auto; + &::-webkit-scrollbar { + display: none; + } + .item { + padding-right: 24px; + padding-left: 24px; + overflow: hidden; + cursor: pointer; + transition: all 0.3s; + + .meta { + width: 100%; + } + + .avatar { + margin-top: 4px; + background: #fff; + } + .iconElement { + font-size: 32px; + } + + &.read { + opacity: 0.4; + } + &:last-child { + border-bottom: 0; + } + &:hover { + background: @primary-1; + } + .title { + margin-bottom: 8px; + font-weight: normal; + } + .description { + font-size: 12px; + line-height: @line-height-base; + } + .datetime { + margin-top: 4px; + font-size: 12px; + line-height: @line-height-base; + } + .extra { + float: right; + margin-top: -1.5px; + margin-right: 0; + color: @text-color-secondary; + font-weight: normal; + } + } + .loadMore { + padding: 8px 0; + color: @primary-6; + text-align: center; + cursor: pointer; + &.loadedAll { + color: rgba(0, 0, 0, 0.25); + cursor: unset; + } + } +} + +.notFound { + padding: 73px 0 88px 0; + color: @text-color-secondary; + text-align: center; + img { + display: inline-block; + height: 76px; + margin-bottom: 16px; + } +} + +.bottomBar { + height: 46px; + color: @text-color; + line-height: 46px; + text-align: center; + border-top: 1px solid @border-color-split; + border-radius: 0 0 @border-radius-base @border-radius-base; + transition: all 0.3s; + div { + display: inline-block; + width: 50%; + cursor: pointer; + transition: all 0.3s; + user-select: none; + &:hover { + color: @heading-color; + } + &:only-child { + width: 100%; + } + &:not(:only-child):last-child { + border-left: 1px solid @border-color-split; + } + } +} diff --git a/src/components/NoticeIcon/NoticeList.tsx b/src/components/NoticeIcon/NoticeList.tsx new file mode 100644 index 00000000..53a58ca1 --- /dev/null +++ b/src/components/NoticeIcon/NoticeList.tsx @@ -0,0 +1,113 @@ +import React from 'react'; +import { Avatar, List } from 'antd'; +import classNames from 'classnames'; +import styles from './NoticeList.less'; +import { NoticeIconData } from './index'; + +export interface NoticeIconTabProps { + count?: number; + list?: NoticeIconData[]; + name?: string; + showClear?: boolean; + showViewMore?: boolean; + style?: React.CSSProperties; + title: string; + tabKey: string; + data?: any[]; + onClick?: (item: any) => void; + onClear?: (item: any) => void; + emptyText?: string; + clearText?: string; + viewMoreText?: string; + onViewMore?: (e: any) => void; +} +const NoticeList: React.SFC = ({ + data = [], + onClick, + onClear, + title, + onViewMore, + emptyText, + showClear = true, + clearText, + viewMoreText, + showViewMore = false, +}) => { + if (data.length === 0) { + return ( +
+ not found +
{emptyText}
+
+ ); + } + return ( +
+ + className={styles.list} + dataSource={data} + renderItem={(item, i) => { + const itemCls = classNames(styles.item, { + [styles.read]: item.read, + }); + // eslint-disable-next-line no-nested-ternary + const leftIcon = item.avatar ? ( + typeof item.avatar === 'string' ? ( + + ) : ( + {item.avatar} + ) + ) : null; + + return ( + onClick && onClick(item)} + > + + {item.title} +
{item.extra}
+
+ } + description={ +
+
{item.description}
+
{item.datetime}
+
+ } + /> + + ); + }} + /> +
+ {showClear ? ( +
+ {clearText} {title} +
+ ) : null} + {showViewMore ? ( +
{ + if (onViewMore) { + onViewMore(e); + } + }} + > + {viewMoreText} +
+ ) : null} +
+ + ); +}; + +export default NoticeList; diff --git a/src/components/NoticeIcon/index.less b/src/components/NoticeIcon/index.less new file mode 100644 index 00000000..1c0593ee --- /dev/null +++ b/src/components/NoticeIcon/index.less @@ -0,0 +1,31 @@ +@import '~antd/lib/style/themes/default.less'; + +.popover { + position: relative; + width: 336px; +} + +.noticeButton { + display: inline-block; + cursor: pointer; + transition: all 0.3s; +} +.icon { + padding: 4px; + vertical-align: middle; +} + +.badge { + font-size: 16px; +} + +.tabs { + :global { + .ant-tabs-nav-scroll { + text-align: center; + } + .ant-tabs-bar { + margin-bottom: 0; + } + } +} diff --git a/src/components/NoticeIcon/index.tsx b/src/components/NoticeIcon/index.tsx new file mode 100644 index 00000000..4ead5bf6 --- /dev/null +++ b/src/components/NoticeIcon/index.tsx @@ -0,0 +1,168 @@ +import React, { Component } from 'react'; +import { Icon, Tabs, Badge, Spin } from 'antd'; +import classNames from 'classnames'; +import HeaderDropdown from '../HeaderDropdown'; +import NoticeList, { NoticeIconTabProps } from './NoticeList'; +import styles from './index.less'; + +const { TabPane } = Tabs; + +export interface NoticeIconData { + avatar?: string | React.ReactNode; + title?: React.ReactNode; + description?: React.ReactNode; + datetime?: React.ReactNode; + extra?: React.ReactNode; + style?: React.CSSProperties; + key?: string | number; + read?: boolean; +} + +export interface NoticeIconProps { + count?: number; + bell?: React.ReactNode; + className?: string; + loading?: boolean; + onClear?: (tabName: string, tabKey: string) => void; + onItemClick?: (item: NoticeIconData, tabProps: NoticeIconTabProps) => void; + onViewMore?: (tabProps: NoticeIconTabProps, e: MouseEvent) => void; + onTabChange?: (tabTile: string) => void; + style?: React.CSSProperties; + onPopupVisibleChange?: (visible: boolean) => void; + popupVisible?: boolean; + clearText?: string; + viewMoreText?: string; + clearClose?: boolean; + children: React.ReactElement[]; +} + +export default class NoticeIcon extends Component { + public static Tab: typeof NoticeList = NoticeList; + + static defaultProps = { + onItemClick: () => {}, + onPopupVisibleChange: () => {}, + onTabChange: () => {}, + onClear: () => {}, + onViewMore: () => {}, + loading: false, + clearClose: false, + emptyImage: 'https://gw.alipayobjects.com/zos/rmsportal/wAhyIChODzsoKIOBHcBk.svg', + }; + + state = { + visible: false, + }; + + onItemClick = (item: NoticeIconData, tabProps: NoticeIconTabProps) => { + const { onItemClick } = this.props; + if (onItemClick) { + onItemClick(item, tabProps); + } + }; + + onClear = (name: string, key: string) => { + const { onClear } = this.props; + if (onClear) { + onClear(name, key); + } + }; + + onTabChange = (tabType: string) => { + const { onTabChange } = this.props; + if (onTabChange) { + onTabChange(tabType); + } + }; + + onViewMore = (tabProps: NoticeIconTabProps, event: MouseEvent) => { + const { onViewMore } = this.props; + if (onViewMore) { + onViewMore(tabProps, event); + } + }; + + getNotificationBox() { + const { children, loading, clearText, viewMoreText } = this.props; + if (!children) { + return null; + } + const panes = React.Children.map(children, (child: React.ReactElement) => { + if (!child) { + return null; + } + const { list, title, count, tabKey, showClear, showViewMore } = child.props; + const len = list && list.length ? list.length : 0; + const msgCount = count || count === 0 ? count : len; + const tabTitle: string = msgCount > 0 ? `${title} (${msgCount})` : title; + return ( + + this.onClear(title, tabKey)} + onClick={item => this.onItemClick(item, child.props)} + onViewMore={event => this.onViewMore(child.props, event)} + showClear={showClear} + showViewMore={showViewMore} + title={title} + {...child.props} + /> + + ); + }); + return ( + + + {panes} + + + ); + } + + handleVisibleChange = (visible: boolean) => { + const { onPopupVisibleChange } = this.props; + this.setState({ visible }); + if (onPopupVisibleChange) { + onPopupVisibleChange(visible); + } + }; + + render() { + 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; + } + return ( + + {trigger} + + ); + } +} diff --git a/src/layouts/BasicLayout.tsx b/src/layouts/BasicLayout.tsx index 154b16f1..f6060c81 100644 --- a/src/layouts/BasicLayout.tsx +++ b/src/layouts/BasicLayout.tsx @@ -1,4 +1,5 @@ import { ConnectState, ConnectProps } from '@/models/connect'; +import RightContent from '@/components/GlobalHeader/RightContent'; import { formatMessage } from 'umi-plugin-react/locale'; import { connect } from 'dva'; import React, { useState } from 'react'; @@ -48,6 +49,7 @@ const BasicLayout: React.FC = props => { }) } onChangeLayoutCollapsed={handleMenuCollapse} + renderRightContent={RightProps => } {...props} > {children}