7 changed files with 460 additions and 32 deletions
@ -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; |
|||
} |
|||
} |
|||
} |
|||
@ -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<NoticeIconTabProps> = ({ |
|||
data = [], |
|||
onClick, |
|||
onClear, |
|||
title, |
|||
onViewMore, |
|||
emptyText, |
|||
showClear = true, |
|||
clearText, |
|||
viewMoreText, |
|||
showViewMore = false, |
|||
}) => { |
|||
if (data.length === 0) { |
|||
return ( |
|||
<div className={styles.notFound}> |
|||
<img |
|||
src="https://gw.alipayobjects.com/zos/rmsportal/sAuJeJzSKbUmHfBQRzmZ.svg" |
|||
alt="not found" |
|||
/> |
|||
<div>{emptyText}</div> |
|||
</div> |
|||
); |
|||
} |
|||
return ( |
|||
<div> |
|||
<List<NoticeIconData> |
|||
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' ? ( |
|||
<Avatar className={styles.avatar} src={item.avatar} /> |
|||
) : ( |
|||
<span className={styles.iconElement}>{item.avatar}</span> |
|||
) |
|||
) : null; |
|||
|
|||
return ( |
|||
<List.Item |
|||
className={itemCls} |
|||
key={item.key || i} |
|||
onClick={() => onClick && onClick(item)} |
|||
> |
|||
<List.Item.Meta |
|||
className={styles.meta} |
|||
avatar={leftIcon} |
|||
title={ |
|||
<div className={styles.title}> |
|||
{item.title} |
|||
<div className={styles.extra}>{item.extra}</div> |
|||
</div> |
|||
} |
|||
description={ |
|||
<div> |
|||
<div className={styles.description}>{item.description}</div> |
|||
<div className={styles.datetime}>{item.datetime}</div> |
|||
</div> |
|||
} |
|||
/> |
|||
</List.Item> |
|||
); |
|||
}} |
|||
/> |
|||
<div className={styles.bottomBar}> |
|||
{showClear ? ( |
|||
<div onClick={onClear}> |
|||
{clearText} {title} |
|||
</div> |
|||
) : null} |
|||
{showViewMore ? ( |
|||
<div |
|||
onClick={e => { |
|||
if (onViewMore) { |
|||
onViewMore(e); |
|||
} |
|||
}} |
|||
> |
|||
{viewMoreText} |
|||
</div> |
|||
) : null} |
|||
</div> |
|||
</div> |
|||
); |
|||
}; |
|||
|
|||
export default NoticeList; |
|||
@ -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; |
|||
} |
|||
} |
|||
} |
|||
@ -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<NoticeIconTabProps>[]; |
|||
} |
|||
|
|||
export default class NoticeIcon extends Component<NoticeIconProps> { |
|||
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<NoticeIconTabProps>) => { |
|||
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 ( |
|||
<TabPane tab={tabTitle} key={title}> |
|||
<NoticeList |
|||
clearText={clearText} |
|||
viewMoreText={viewMoreText} |
|||
data={list} |
|||
onClear={() => 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} |
|||
/> |
|||
</TabPane> |
|||
); |
|||
}); |
|||
return ( |
|||
<Spin spinning={loading} delay={0}> |
|||
<Tabs className={styles.tabs} onChange={this.onTabChange}> |
|||
{panes} |
|||
</Tabs> |
|||
</Spin> |
|||
); |
|||
} |
|||
|
|||
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 || <Icon type="bell" className={styles.icon} />; |
|||
const trigger = ( |
|||
<span className={classNames(noticeButtonClass, { opened: visible })}> |
|||
<Badge count={count} style={{ boxShadow: 'none' }} className={styles.badge}> |
|||
{NoticeBellIcon} |
|||
</Badge> |
|||
</span> |
|||
); |
|||
if (!notificationBox) { |
|||
return trigger; |
|||
} |
|||
const popoverProps: { |
|||
visible?: boolean; |
|||
} = {}; |
|||
if ('popupVisible' in this.props) { |
|||
popoverProps.visible = popupVisible; |
|||
} |
|||
return ( |
|||
<HeaderDropdown |
|||
placement="bottomRight" |
|||
overlay={notificationBox} |
|||
overlayClassName={styles.popover} |
|||
trigger={['click']} |
|||
visible={visible} |
|||
onVisibleChange={this.handleVisibleChange} |
|||
{...popoverProps} |
|||
> |
|||
{trigger} |
|||
</HeaderDropdown> |
|||
); |
|||
} |
|||
} |
|||
Loading…
Reference in new issue