diff --git a/src/components/Charts/TimelineChart/index.js b/src/components/Charts/TimelineChart/index.js index b76e782..d82623c 100644 --- a/src/components/Charts/TimelineChart/index.js +++ b/src/components/Charts/TimelineChart/index.js @@ -17,15 +17,11 @@ class TimelineChart extends React.Component { y2: 'y2', }, borderWidth = 2, - data = [ - { - x: 0, - y1: 0, - y2: 0, - }, - ], + data: sourceData, } = this.props; + const data = Array.isArray(sourceData) ? sourceData : [{ x: 0, y1: 0, y2: 0 }]; + data.sort((a, b) => a.x - b.x); let max; diff --git a/src/components/Ellipsis/index.d.ts b/src/components/Ellipsis/index.d.ts index 6c95bc0..37d508d 100644 --- a/src/components/Ellipsis/index.d.ts +++ b/src/components/Ellipsis/index.d.ts @@ -16,6 +16,6 @@ export interface IEllipsisProps { } export function getStrFullLength(str: string): number; -export function cutStrByFullLength(str: string, maxLength: number): number; +export function cutStrByFullLength(str: string, maxLength: number): string; export default class Ellipsis extends React.Component {} diff --git a/src/components/GlobalHeader/RightContent.js b/src/components/GlobalHeader/RightContent.js index bc67dab..497ca50 100644 --- a/src/components/GlobalHeader/RightContent.js +++ b/src/components/GlobalHeader/RightContent.js @@ -63,13 +63,30 @@ export default class GlobalHeaderRight extends PureComponent { }); }; + fetchMoreNotices = tabProps => { + const { list, name } = tabProps; + const { dispatch, notices = [] } = this.props; + const lastItemId = notices[notices.length - 1].id; + dispatch({ + type: 'global/fetchMoreNotices', + payload: { + lastItemId, + type: name, + offset: list.length, + }, + }); + }; + render() { const { currentUser, + fetchingMoreNotices, fetchingNotices, + loadedAllNotices, onNoticeVisibleChange, onMenuClick, onNoticeClear, + skeletonCount, theme, } = this.props; const menu = ( @@ -94,6 +111,11 @@ export default class GlobalHeaderRight extends PureComponent { ); + const loadMoreProps = { + skeletonCount, + loadedAll: loadedAllNotices, + loading: fetchingMoreNotices, + }; const noticeData = this.getNoticeData(); const unreadMsg = this.getUnreadData(noticeData); let className = styles.right; @@ -137,8 +159,11 @@ export default class GlobalHeaderRight extends PureComponent { locale={{ emptyText: formatMessage({ id: 'component.noticeIcon.empty' }), clear: formatMessage({ id: 'component.noticeIcon.clear' }), + loadedAll: formatMessage({ id: 'component.noticeIcon.loaded' }), + loadMore: formatMessage({ id: 'component.noticeIcon.loading-more' }), }} onClear={onNoticeClear} + onLoadMore={this.fetchMoreNotices} onPopupVisibleChange={onNoticeVisibleChange} loading={fetchingNotices} clearClose @@ -150,6 +175,7 @@ export default class GlobalHeaderRight extends PureComponent { name="notification" emptyText={formatMessage({ id: 'component.globalHeader.notification.empty' })} emptyImage="https://gw.alipayobjects.com/zos/rmsportal/wAhyIChODzsoKIOBHcBk.svg" + {...loadMoreProps} /> {currentUser.name ? ( diff --git a/src/components/HeaderSearch/index.js b/src/components/HeaderSearch/index.js index 95d946c..04f8b38 100644 --- a/src/components/HeaderSearch/index.js +++ b/src/components/HeaderSearch/index.js @@ -11,6 +11,7 @@ export default class HeaderSearch extends PureComponent { className: PropTypes.string, placeholder: PropTypes.string, onSearch: PropTypes.func, + onChange: PropTypes.func, onPressEnter: PropTypes.func, defaultActiveFirstOption: PropTypes.bool, dataSource: PropTypes.array, @@ -22,6 +23,7 @@ export default class HeaderSearch extends PureComponent { defaultActiveFirstOption: false, onPressEnter: () => {}, onSearch: () => {}, + onChange: () => {}, className: '', placeholder: '', dataSource: [], @@ -61,8 +63,11 @@ export default class HeaderSearch extends PureComponent { }; onChange = value => { - const { onChange } = this.props; + const { onSearch, onChange } = this.props; this.setState({ value }); + if (onSearch) { + onSearch(value); + } if (onChange) { onChange(value); } diff --git a/src/components/NoticeIcon/NoticeIconTab.d.ts b/src/components/NoticeIcon/NoticeIconTab.d.ts index a44ecd9..6ffaf03 100644 --- a/src/components/NoticeIcon/NoticeIconTab.d.ts +++ b/src/components/NoticeIcon/NoticeIconTab.d.ts @@ -1,4 +1,6 @@ +import { SkeletonProps } from 'antd/lib/skeleton'; import * as React from 'react'; + export interface INoticeIconData { avatar?: string | React.ReactNode; title?: React.ReactNode; @@ -9,14 +11,18 @@ export interface INoticeIconData { } export interface INoticeIconTabProps { - list?: INoticeIconData[]; count?: number; - title?: string; - name?: string; emptyText?: React.ReactNode; emptyImage?: string; - style?: React.CSSProperties; + list?: INoticeIconData[]; + loadedAll?: boolean; + loading?: boolean; + name?: string; showClear?: boolean; + skeletonCount?: number; + skeletonProps?: SkeletonProps; + style?: React.CSSProperties; + title?: string; } export default class NoticeIconTab extends React.Component {} diff --git a/src/components/NoticeIcon/NoticeList.js b/src/components/NoticeIcon/NoticeList.js index d0de9af..6b73e6e 100644 --- a/src/components/NoticeIcon/NoticeList.js +++ b/src/components/NoticeIcon/NoticeList.js @@ -1,8 +1,10 @@ import React from 'react'; -import { Avatar, List } from 'antd'; +import { Avatar, List, Skeleton } from 'antd'; import classNames from 'classnames'; import styles from './NoticeList.less'; +let ListElement = null; + export default function NoticeList({ data = [], onClick, @@ -11,7 +13,14 @@ export default function NoticeList({ locale, emptyText, emptyImage, + loading, + onLoadMore, + visible, + loadedAll = true, + scrollToLoad = true, showClear = true, + skeletonCount = 5, + skeletonProps = {}, }) { if (data.length === 0) { return ( @@ -21,10 +30,36 @@ export default function NoticeList({ ); } + const loadingList = Array.from({ length: loading ? skeletonCount : 0 }).map(() => ({ loading })); + const LoadMore = loadedAll ? ( +
+ {locale.loadedAll} +
+ ) : ( +
+ {locale.loadMore} +
+ ); + const onScroll = event => { + if (!scrollToLoad || loading || loadedAll) return; + if (typeof onLoadMore !== 'function') return; + const { currentTarget: t } = event; + if (t.scrollHeight - t.scrollTop - t.clientHeight <= 40) { + onLoadMore(event); + ListElement = t; + } + }; + if (!visible && ListElement) { + try { + ListElement.scrollTo(null, 0); + } catch (err) { + ListElement = null; + } + } return (
- - {data.map((item, i) => { + + {[...data, ...loadingList].map((item, i) => { const itemCls = classNames(styles.item, { [styles.read]: item.read, }); @@ -33,30 +68,32 @@ export default function NoticeList({ typeof item.avatar === 'string' ? ( ) : ( - item.avatar + {item.avatar} ) ) : null; return ( onClick(item)}> - {leftIcon}} - title={ -
- {item.title} -
{item.extra}
-
- } - description={ -
-
- {item.description} + + + {item.title} +
{item.extra}
-
{item.datetime}
-
- } - /> + } + description={ +
+
+ {item.description} +
+
{item.datetime}
+
+ } + /> +
); })} diff --git a/src/components/NoticeIcon/NoticeList.less b/src/components/NoticeIcon/NoticeList.less index e34efdc..8435414 100644 --- a/src/components/NoticeIcon/NoticeList.less +++ b/src/components/NoticeIcon/NoticeList.less @@ -3,6 +3,9 @@ .list { max-height: 400px; overflow: auto; + &::-webkit-scrollbar { + display: none; + } .item { transition: all 0.3s; overflow: hidden; @@ -52,6 +55,16 @@ margin-top: -1.5px; } } + .loadMore { + padding: 8px 0; + cursor: pointer; + color: @primary-6; + text-align: center; + &.loadedAll { + cursor: unset; + color: rgba(0, 0, 0, 0.25); + } + } } .notFound { diff --git a/src/components/NoticeIcon/index.d.ts b/src/components/NoticeIcon/index.d.ts index 79803fe..f7d6479 100644 --- a/src/components/NoticeIcon/index.d.ts +++ b/src/components/NoticeIcon/index.d.ts @@ -8,11 +8,17 @@ export interface INoticeIconProps { loading?: boolean; onClear?: (tabName: string) => void; onItemClick?: (item: INoticeIconData, tabProps: INoticeIconProps) => void; + onLoadMore?: (tabProps: INoticeIconProps) => void; onTabChange?: (tabTile: string) => void; style?: React.CSSProperties; onPopupVisibleChange?: (visible: boolean) => void; popupVisible?: boolean; - locale?: { emptyText: string; clear: string }; + locale?: { + emptyText: string; + clear: string; + loadedAll: string; + loadMore: string; + }; clearClose?: boolean; } diff --git a/src/components/NoticeIcon/index.en-US.md b/src/components/NoticeIcon/index.en-US.md index c4990b6..a5a5f21 100644 --- a/src/components/NoticeIcon/index.en-US.md +++ b/src/components/NoticeIcon/index.en-US.md @@ -13,32 +13,40 @@ Property | Description | Type | Default ----|------|-----|------ count | Total number of messages | number | - bell | Change the bell Icon | ReactNode | `` -loading | Popup card loading status | boolean | false -onClear | Click to clear button the callback | function(tabName) | - +loading | Popup card loading status | boolean | `false` +onClear | Click to clear button the callback | function(tabName) | - onItemClick | Click on the list item's callback | function(item, tabProps) | - -onTabChange | Switching callbacks for tabs | function(tabTitle) | - +onLoadMore | Callback of click for loading more | function(tabProps, event) | - onPopupVisibleChange | Popup Card Showing or Hiding Callbacks | function(visible) | - +onTabChange | Switching callbacks for tabs | function(tabTitle) | - popupVisible | Popup card display state | boolean | - -locale | Default message text | Object | `{ emptyText: '暂无数据', clear: '清空' }` +locale | Default message text | Object | `{ emptyText: 'No notifications', clear: 'Clear', loadedAll: 'Loaded', loadMore: 'Loading more' }` +clearClose | Close menu after clear | boolean | `false` ### NoticeIcon.Tab Property | Description | Type | Default ----|------|-----|------ -title | header for message Tab | string | - -name | identifier for message Tab | string | - +count | Unread messages count of this tab | number | list.length +emptyText | Message text when list is empty | ReactNode | - +emptyImage | Image when list is empty | string | - list | List data, format refer to the following table | Array | `[]` -showClear | Clear button display status | boolean | true -emptyText | message text when list is empty | ReactNode | - -emptyImage | image when list is empty | string | - - +loadedAll | All messages have been loaded | boolean | `true` +loading | Loading status of this tab | boolean | `false` +name | identifier for message Tab | string | - +scrollToLoad | Scroll to load | boolean | `true` +skeletonCount | Number of skeleton when tab is loading | number | `5` +skeletonProps | Props of skeleton | SkeletonProps | `{}` +showClear | Clear button display status | boolean | `true` +title | header for message Tab | string | - ### Tab data Property | Description | Type | Default ----|------|-----|------ -avatar | avatar img url | string \| ReactNode | - +avatar | avatar img url | string \| ReactNode | - title | title | ReactNode | - description | description info | ReactNode | - datetime | Timestamps | ReactNode | - -extra |Additional information in the upper right corner of the list item | ReactNode | - +extra | Additional information in the upper right corner of the list item | ReactNode | - +clickClose | Close menu after clicking list item | boolean | `false` diff --git a/src/components/NoticeIcon/index.js b/src/components/NoticeIcon/index.js index ad21287..133819b 100644 --- a/src/components/NoticeIcon/index.js +++ b/src/components/NoticeIcon/index.js @@ -21,6 +21,8 @@ export default class NoticeIcon extends PureComponent { locale: { emptyText: 'No notifications', clear: 'Clear', + loadedAll: 'Loaded', + loadMore: 'Loading more', }, emptyImage: 'https://gw.alipayobjects.com/zos/rmsportal/wAhyIChODzsoKIOBHcBk.svg', }; @@ -51,25 +53,53 @@ export default class NoticeIcon extends PureComponent { onTabChange(tabType); }; + onLoadMore = (tabProps, event) => { + const { onLoadMore } = this.props; + onLoadMore(tabProps, event); + }; + getNotificationBox() { + const { visible } = this.state; const { children, loading, locale } = this.props; if (!children) { return null; } const panes = React.Children.map(children, child => { - const { list, title, name, count } = child.props; + const { + list, + title, + name, + count, + emptyText, + emptyImage, + showClear, + loadedAll, + scrollToLoad, + skeletonCount, + skeletonProps, + loading: tabLoading, + } = child.props; const len = list && list.length ? list.length : 0; const msgCount = count || count === 0 ? count : len; const tabTitle = msgCount > 0 ? `${title} (${msgCount})` : title; return ( this.onItemClick(item, child.props)} - onClear={() => this.onClear(name)} - title={title} + emptyImage={emptyImage} + emptyText={emptyText} + loadedAll={loadedAll} + loading={tabLoading} locale={locale} + onClear={() => this.onClear(name)} + onClick={item => this.onItemClick(item, child.props)} + onLoadMore={event => this.onLoadMore(child.props, event)} + scrollToLoad={scrollToLoad} + showClear={showClear} + skeletonCount={skeletonCount} + skeletonProps={skeletonProps} + title={title} + visible={visible} /> ); diff --git a/src/components/NoticeIcon/index.less b/src/components/NoticeIcon/index.less index 55eb929..3ca7c5d 100644 --- a/src/components/NoticeIcon/index.less +++ b/src/components/NoticeIcon/index.less @@ -20,7 +20,7 @@ text-align: center; } .ant-tabs-bar { - margin-bottom: 4px; + margin-bottom: 0; } } } diff --git a/src/components/NoticeIcon/index.zh-CN.md b/src/components/NoticeIcon/index.zh-CN.md index 65b2db7..23dab22 100644 --- a/src/components/NoticeIcon/index.zh-CN.md +++ b/src/components/NoticeIcon/index.zh-CN.md @@ -13,26 +13,32 @@ order: 9 ----|------|-----|------ count | 图标上的消息总数 | number | - bell | translate this please -> Change the bell Icon | ReactNode | `` -loading | 弹出卡片加载状态 | boolean | false +loading | 弹出卡片加载状态 | boolean | `false` onClear | 点击清空按钮的回调 | function(tabName) | - onItemClick | 点击列表项的回调 | function(item, tabProps) | - -onTabChange | 切换页签的回调 | function(tabTitle) | - +onLoadMore | 加载更多的回调 | function(tabProps, event) | - onPopupVisibleChange | 弹出卡片显隐的回调 | function(visible) | - +onTabChange | 切换页签的回调 | function(tabTitle) | - popupVisible | 控制弹层显隐 | boolean | - -locale | 默认文案 | Object | `{ emptyText: '暂无数据', clear: '清空' }` -clearClose | 点击清空按钮后关闭通知菜单 | boolean | false +locale | 默认文案 | Object | `{ emptyText: 'No notifications', clear: 'Clear', loadedAll: 'Loaded', loadMore: 'Loading more' }` +clearClose | 点击清空按钮后关闭通知菜单 | boolean | `false` ### NoticeIcon.Tab 参数 | 说明 | 类型 | 默认值 ----|------|-----|------ -title | 消息分类的页签标题 | string | - -name | 消息分类的标识符 | string | - -list | 列表数据,格式参照下表 | Array | `[]` -showClear | 是否显示清空按钮 | boolean | true +count | 当前 Tab 未读消息数量 | number | list.length emptyText | 针对每个 Tab 定制空数据文案 | ReactNode | - emptyImage | 针对每个 Tab 定制空数据图片 | string | - - +list | 列表数据,格式参照下表 | Array | `[]` +loadedAll | 已加载完所有消息 | boolean | `true` +loading | 当前 Tab 的加载状态 | boolean | `false` +name | 消息分类的标识符 | string | - +scrollToLoad | 允许滚动自加载 | boolean | `true` +skeletonCount | 加载时占位骨架的数量 | number | `5` +skeletonProps | 加载时占位骨架的属性 | SkeletonProps | `{}` +showClear | 是否显示清空按钮 | boolean | `true` +title | 消息分类的页签标题 | string | - ### Tab data @@ -43,4 +49,4 @@ title | 标题 | ReactNode | - description | 描述信息 | ReactNode | - datetime | 时间戳 | ReactNode | - extra | 额外信息,在列表项右上角 | ReactNode | - -clickClose | 点击列表项关闭通知菜单 | boolean | false +clickClose | 点击列表项关闭通知菜单 | boolean | `false` diff --git a/src/components/SiderMenu/SiderMenu.js b/src/components/SiderMenu/SiderMenu.js index 2d36d6e..e767bf8 100644 --- a/src/components/SiderMenu/SiderMenu.js +++ b/src/components/SiderMenu/SiderMenu.js @@ -68,7 +68,7 @@ export default class SiderMenu extends PureComponent { }> diff --git a/src/components/TopNavHeader/index.js b/src/components/TopNavHeader/index.js index 7abd8b8..8ae89e8 100644 --- a/src/components/TopNavHeader/index.js +++ b/src/components/TopNavHeader/index.js @@ -32,7 +32,7 @@ export default class TopNavHeader extends PureComponent {