Commit 24e01da4 authored by 何乐's avatar 何乐 Committed by 陈帅

LoadMore for NoticeIcon (#3221)

* add LoadMore for NoticeIcon

* fix some bugs

* add demo in GlobalHeader

* update docs && change some props' name

* fix some bugs

* fix some bugs

* lint markdown files

* lint markdown files

* 修复 NoticeIcon 列表 Avatar align 问题

* fix .md files

* looking for errors in ci

* add scrollToLoad

* add LoadMore for NoticeIcon

* fix some bugs

* add demo in GlobalHeader

* update docs && change some props' name

* fix some bugs

* fix some bugs

* lint markdown files

* lint markdown files

* 修复 NoticeIcon 列表 Avatar align 问题

* fix .md files

* looking for errors in ci

* add scrollToLoad

* fix: onLoadMore()

* update document

* fix markdown files @NoticeIcon
parent 2f51506c
const getNotices = (req, res) => const fakeNotices = [
res.json([ {
{ id: '000000001',
id: '000000001', avatar: 'https://gw.alipayobjects.com/zos/rmsportal/ThXAXghbEsBCCSDihZxY.png',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/ThXAXghbEsBCCSDihZxY.png', title: '你收到了 14 份新周报',
title: '你收到了 14 份新周报', datetime: '2017-08-09',
datetime: '2017-08-09', type: 'notification',
type: 'notification', },
}, {
{ id: '000000002',
id: '000000002', avatar: 'https://gw.alipayobjects.com/zos/rmsportal/OKJXDXrmkNshAMvwtvhu.png',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/OKJXDXrmkNshAMvwtvhu.png', title: '你推荐的 曲妮妮 已通过第三轮面试',
title: '你推荐的 曲妮妮 已通过第三轮面试', datetime: '2017-08-08',
datetime: '2017-08-08', type: 'notification',
type: 'notification', },
}, {
{ id: '000000003',
id: '000000003', avatar: 'https://gw.alipayobjects.com/zos/rmsportal/kISTdvpyTAhtGxpovNWd.png',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/kISTdvpyTAhtGxpovNWd.png', title: '这种模板可以区分多种通知类型',
title: '这种模板可以区分多种通知类型', datetime: '2017-08-07',
datetime: '2017-08-07', read: true,
read: true, type: 'notification',
type: 'notification', },
}, {
{ id: '000000004',
id: '000000004', avatar: 'https://gw.alipayobjects.com/zos/rmsportal/GvqBnKhFgObvnSGkDsje.png',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/GvqBnKhFgObvnSGkDsje.png', title: '左侧图标用于区分不同的类型',
title: '左侧图标用于区分不同的类型', datetime: '2017-08-07',
datetime: '2017-08-07', type: 'notification',
type: 'notification', },
}, {
{ id: '000000005',
id: '000000005', avatar: 'https://gw.alipayobjects.com/zos/rmsportal/ThXAXghbEsBCCSDihZxY.png',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/ThXAXghbEsBCCSDihZxY.png', title: '内容不要超过两行字,超出时自动截断',
title: '内容不要超过两行字,超出时自动截断', datetime: '2017-08-07',
datetime: '2017-08-07', type: 'notification',
type: 'notification', },
}, {
{ id: '000000006',
id: '000000006', avatar: 'https://gw.alipayobjects.com/zos/rmsportal/fcHMVNCjPOsbUGdEduuv.jpeg',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/fcHMVNCjPOsbUGdEduuv.jpeg', title: '曲丽丽 评论了你',
title: '曲丽丽 评论了你', description: '描述信息描述信息描述信息',
description: '描述信息描述信息描述信息', datetime: '2017-08-07',
datetime: '2017-08-07', type: 'message',
type: 'message', clickClose: true,
clickClose: true, },
}, {
{ id: '000000007',
id: '000000007', avatar: 'https://gw.alipayobjects.com/zos/rmsportal/fcHMVNCjPOsbUGdEduuv.jpeg',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/fcHMVNCjPOsbUGdEduuv.jpeg', title: '朱偏右 回复了你',
title: '朱偏右 回复了你', description: '这种模板用于提醒谁与你发生了互动,左侧放『谁』的头像',
description: '这种模板用于提醒谁与你发生了互动,左侧放『谁』的头像', datetime: '2017-08-07',
datetime: '2017-08-07', type: 'message',
type: 'message', clickClose: true,
clickClose: true, },
}, {
{ id: '000000008',
id: '000000008', avatar: 'https://gw.alipayobjects.com/zos/rmsportal/fcHMVNCjPOsbUGdEduuv.jpeg',
avatar: 'https://gw.alipayobjects.com/zos/rmsportal/fcHMVNCjPOsbUGdEduuv.jpeg', title: '标题',
title: '标题', description: '这种模板用于提醒谁与你发生了互动,左侧放『谁』的头像',
description: '这种模板用于提醒谁与你发生了互动,左侧放『谁』的头像', datetime: '2017-08-07',
datetime: '2017-08-07', type: 'message',
type: 'message', clickClose: true,
clickClose: true, },
}, {
{ id: '000000009',
id: '000000009', title: '任务名称',
title: '任务名称', description: '任务需要在 2017-01-12 20:00 前启动',
description: '任务需要在 2017-01-12 20:00 前启动', extra: '未开始',
extra: '未开始', status: 'todo',
status: 'todo', type: 'event',
type: 'event', },
}, {
{ id: '000000010',
id: '000000010', title: '第三方紧急代码变更',
title: '第三方紧急代码变更', description: '冠霖提交于 2017-01-06,需在 2017-01-07 前完成代码变更任务',
description: '冠霖提交于 2017-01-06,需在 2017-01-07 前完成代码变更任务', extra: '马上到期',
extra: '马上到期', status: 'urgent',
status: 'urgent', type: 'event',
type: 'event', },
}, {
{ id: '000000011',
id: '000000011', title: '信息安全考试',
title: '信息安全考试', description: '指派竹尔于 2017-01-09 前完成更新并发布',
description: '指派竹尔于 2017-01-09 前完成更新并发布', extra: '已耗时 8 天',
extra: '已耗时 8 天', status: 'doing',
status: 'doing', type: 'event',
type: 'event', },
}, {
{ id: '000000012',
id: '000000012', title: 'ABCD 版本发布',
title: 'ABCD 版本发布', description: '冠霖提交于 2017-01-06,需在 2017-01-07 前完成代码变更任务',
description: '冠霖提交于 2017-01-06,需在 2017-01-07 前完成代码变更任务', extra: '进行中',
extra: '进行中', status: 'processing',
status: 'processing', type: 'event',
type: 'event', },
}, ];
]);
const getNotices = (req, res) => {
if (req.query && req.query.type) {
const startFrom = parseInt(req.query.lastItemId, 10) + 1;
const result = fakeNotices
.filter(({ type }) => type === req.query.type)
.map((notice, index) => ({
...notice,
id: `0000000${startFrom + index}`,
}));
return res.json(startFrom > 24 ? result.concat(null) : result);
}
return res.json(fakeNotices);
};
export default { export default {
'GET /api/notices': getNotices, 'GET /api/notices': getNotices,
......
...@@ -63,13 +63,30 @@ export default class GlobalHeaderRight extends PureComponent { ...@@ -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() { render() {
const { const {
currentUser, currentUser,
fetchingMoreNotices,
fetchingNotices, fetchingNotices,
loadedAllNotices,
onNoticeVisibleChange, onNoticeVisibleChange,
onMenuClick, onMenuClick,
onNoticeClear, onNoticeClear,
skeletonCount,
theme, theme,
} = this.props; } = this.props;
const menu = ( const menu = (
...@@ -93,6 +110,11 @@ export default class GlobalHeaderRight extends PureComponent { ...@@ -93,6 +110,11 @@ export default class GlobalHeaderRight extends PureComponent {
</Menu.Item> </Menu.Item>
</Menu> </Menu>
); );
const loadMoreProps = {
skeletonCount,
loadedAll: loadedAllNotices,
loading: fetchingMoreNotices,
};
const noticeData = this.getNoticeData(); const noticeData = this.getNoticeData();
const unreadMsg = this.getUnreadData(noticeData); const unreadMsg = this.getUnreadData(noticeData);
let className = styles.right; let className = styles.right;
...@@ -136,8 +158,11 @@ export default class GlobalHeaderRight extends PureComponent { ...@@ -136,8 +158,11 @@ export default class GlobalHeaderRight extends PureComponent {
locale={{ locale={{
emptyText: formatMessage({ id: 'component.noticeIcon.empty' }), emptyText: formatMessage({ id: 'component.noticeIcon.empty' }),
clear: formatMessage({ id: 'component.noticeIcon.clear' }), clear: formatMessage({ id: 'component.noticeIcon.clear' }),
loadedAll: formatMessage({ id: 'component.noticeIcon.loaded' }),
loadMore: formatMessage({ id: 'component.noticeIcon.loading-more' }),
}} }}
onClear={onNoticeClear} onClear={onNoticeClear}
onLoadMore={this.fetchMoreNotices}
onPopupVisibleChange={onNoticeVisibleChange} onPopupVisibleChange={onNoticeVisibleChange}
loading={fetchingNotices} loading={fetchingNotices}
clearClose clearClose
...@@ -149,6 +174,7 @@ export default class GlobalHeaderRight extends PureComponent { ...@@ -149,6 +174,7 @@ export default class GlobalHeaderRight extends PureComponent {
name="notification" name="notification"
emptyText={formatMessage({ id: 'component.globalHeader.notification.empty' })} emptyText={formatMessage({ id: 'component.globalHeader.notification.empty' })}
emptyImage="https://gw.alipayobjects.com/zos/rmsportal/wAhyIChODzsoKIOBHcBk.svg" emptyImage="https://gw.alipayobjects.com/zos/rmsportal/wAhyIChODzsoKIOBHcBk.svg"
{...loadMoreProps}
/> />
<NoticeIcon.Tab <NoticeIcon.Tab
count={unreadMsg.message} count={unreadMsg.message}
...@@ -157,6 +183,7 @@ export default class GlobalHeaderRight extends PureComponent { ...@@ -157,6 +183,7 @@ export default class GlobalHeaderRight extends PureComponent {
name="message" name="message"
emptyText={formatMessage({ id: 'component.globalHeader.message.empty' })} emptyText={formatMessage({ id: 'component.globalHeader.message.empty' })}
emptyImage="https://gw.alipayobjects.com/zos/rmsportal/sAuJeJzSKbUmHfBQRzmZ.svg" emptyImage="https://gw.alipayobjects.com/zos/rmsportal/sAuJeJzSKbUmHfBQRzmZ.svg"
{...loadMoreProps}
/> />
<NoticeIcon.Tab <NoticeIcon.Tab
count={unreadMsg.event} count={unreadMsg.event}
...@@ -165,6 +192,7 @@ export default class GlobalHeaderRight extends PureComponent { ...@@ -165,6 +192,7 @@ export default class GlobalHeaderRight extends PureComponent {
name="event" name="event"
emptyText={formatMessage({ id: 'component.globalHeader.event.empty' })} emptyText={formatMessage({ id: 'component.globalHeader.event.empty' })}
emptyImage="https://gw.alipayobjects.com/zos/rmsportal/HsIsxMZiWKrNUavQUXqx.svg" emptyImage="https://gw.alipayobjects.com/zos/rmsportal/HsIsxMZiWKrNUavQUXqx.svg"
{...loadMoreProps}
/> />
</NoticeIcon> </NoticeIcon>
{currentUser.name ? ( {currentUser.name ? (
......
import { SkeletonProps } from 'antd/lib/skeleton';
import * as React from 'react'; import * as React from 'react';
export interface INoticeIconData { export interface INoticeIconData {
avatar?: string | React.ReactNode; avatar?: string | React.ReactNode;
title?: React.ReactNode; title?: React.ReactNode;
...@@ -9,14 +11,18 @@ export interface INoticeIconData { ...@@ -9,14 +11,18 @@ export interface INoticeIconData {
} }
export interface INoticeIconTabProps { export interface INoticeIconTabProps {
list?: INoticeIconData[];
count?: number; count?: number;
title?: string;
name?: string;
emptyText?: React.ReactNode; emptyText?: React.ReactNode;
emptyImage?: string; emptyImage?: string;
style?: React.CSSProperties; list?: INoticeIconData[];
loadedAll?: boolean;
loading?: boolean;
name?: string;
showClear?: boolean; showClear?: boolean;
skeletonCount?: number;
skeletonProps: SkeletonProps;
style?: React.CSSProperties;
title?: string;
} }
export default class NoticeIconTab extends React.Component<INoticeIconTabProps, any> {} export default class NoticeIconTab extends React.Component<INoticeIconTabProps, any> {}
import React from 'react'; import React from 'react';
import { Avatar, List } from 'antd'; import { Avatar, List, Skeleton } from 'antd';
import classNames from 'classnames'; import classNames from 'classnames';
import styles from './NoticeList.less'; import styles from './NoticeList.less';
let ListElement = null;
export default function NoticeList({ export default function NoticeList({
data = [], data = [],
onClick, onClick,
...@@ -11,7 +13,14 @@ export default function NoticeList({ ...@@ -11,7 +13,14 @@ export default function NoticeList({
locale, locale,
emptyText, emptyText,
emptyImage, emptyImage,
loading,
onLoadMore,
visible,
loadedAll = true,
scrollToLoad = true,
showClear = true, showClear = true,
skeletonCount = 5,
skeletonProps = {},
}) { }) {
if (data.length === 0) { if (data.length === 0) {
return ( return (
...@@ -21,10 +30,36 @@ export default function NoticeList({ ...@@ -21,10 +30,36 @@ export default function NoticeList({
</div> </div>
); );
} }
const loadingList = Array.from({ length: loading ? skeletonCount : 0 }).map(() => ({ loading }));
const LoadMore = loadedAll ? (
<div className={classNames(styles.loadMore, styles.loadedAll)}>
<span>{locale.loadedAll}</span>
</div>
) : (
<div className={styles.loadMore} onClick={onLoadMore}>
<span>{locale.loadMore}</span>
</div>
);
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 ( return (
<div> <div>
<List className={styles.list}> <List className={styles.list} loadMore={LoadMore} onScroll={onScroll}>
{data.map((item, i) => { {[...data, ...loadingList].map((item, i) => {
const itemCls = classNames(styles.item, { const itemCls = classNames(styles.item, {
[styles.read]: item.read, [styles.read]: item.read,
}); });
...@@ -33,30 +68,32 @@ export default function NoticeList({ ...@@ -33,30 +68,32 @@ export default function NoticeList({
typeof item.avatar === 'string' ? ( typeof item.avatar === 'string' ? (
<Avatar className={styles.avatar} src={item.avatar} /> <Avatar className={styles.avatar} src={item.avatar} />
) : ( ) : (
item.avatar <span className={styles.iconElement}>{item.avatar}</span>
) )
) : null; ) : null;
return ( return (
<List.Item className={itemCls} key={item.key || i} onClick={() => onClick(item)}> <List.Item className={itemCls} key={item.key || i} onClick={() => onClick(item)}>
<List.Item.Meta <Skeleton avatar title={false} active {...skeletonProps} loading={item.loading}>
className={styles.meta} <List.Item.Meta
avatar={<span className={styles.iconElement}>{leftIcon}</span>} className={styles.meta}
title={ avatar={leftIcon}
<div className={styles.title}> title={
{item.title} <div className={styles.title}>
<div className={styles.extra}>{item.extra}</div> {item.title}
</div> <div className={styles.extra}>{item.extra}</div>
} </div>
description={ }
<div> description={
<div className={styles.description} title={item.description}> <div>
{item.description} <div className={styles.description} title={item.description}>
{item.description}
</div>
<div className={styles.datetime}>{item.datetime}</div>
</div> </div>
<div className={styles.datetime}>{item.datetime}</div> }
</div> />
} </Skeleton>
/>
</List.Item> </List.Item>
); );
})} })}
......
...@@ -3,6 +3,9 @@ ...@@ -3,6 +3,9 @@
.list { .list {
max-height: 400px; max-height: 400px;
overflow: auto; overflow: auto;
&::-webkit-scrollbar {
display: none;
}
.item { .item {
transition: all 0.3s; transition: all 0.3s;
overflow: hidden; overflow: hidden;
...@@ -52,6 +55,16 @@ ...@@ -52,6 +55,16 @@
margin-top: -1.5px; 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 { .notFound {
......
...@@ -8,11 +8,17 @@ export interface INoticeIconProps { ...@@ -8,11 +8,17 @@ export interface INoticeIconProps {
loading?: boolean; loading?: boolean;
onClear?: (tabName: string) => void; onClear?: (tabName: string) => void;
onItemClick?: (item: INoticeIconData, tabProps: INoticeIconProps) => void; onItemClick?: (item: INoticeIconData, tabProps: INoticeIconProps) => void;
onLoadMore?: (tabProps: INoticeIconProps) => void;
onTabChange?: (tabTile: string) => void; onTabChange?: (tabTile: string) => void;
style?: React.CSSProperties; style?: React.CSSProperties;
onPopupVisibleChange?: (visible: boolean) => void; onPopupVisibleChange?: (visible: boolean) => void;
popupVisible?: boolean; popupVisible?: boolean;
locale?: { emptyText: string; clear: string }; locale?: {
emptyText: string;
clear: string;
loadedAll: string;
loadMore: string;
};
clearClose?: boolean; clearClose?: boolean;
} }
......
...@@ -13,32 +13,40 @@ Property | Description | Type | Default ...@@ -13,32 +13,40 @@ Property | Description | Type | Default
----|------|-----|------ ----|------|-----|------
count | Total number of messages | number | - count | Total number of messages | number | -
bell | Change the bell Icon | ReactNode | `<Icon type='bell' />` bell | Change the bell Icon | ReactNode | `<Icon type='bell' />`
loading | Popup card loading status | boolean | false loading | Popup card loading status | boolean | `false`
onClear | Click to clear button the callback | function(tabName) | - onClear | Click to clear button the callback | function(tabName) | -
onItemClick | Click on the list item's callback | function(item, tabProps) | - 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) | - onPopupVisibleChange | Popup Card Showing or Hiding Callbacks | function(visible) | -
onTabChange | Switching callbacks for tabs | function(tabTitle) | -
popupVisible | Popup card display state | boolean | - 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 ### NoticeIcon.Tab
Property | Description | Type | Default Property | Description | Type | Default
----|------|-----|------ ----|------|-----|------
title | header for message Tab | string | - count | Unread messages count of this tab | number | list.length
name | identifier for message Tab | string | - 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 | `[]` list | List data, format refer to the following table | Array | `[]`
showClear | Clear button display status | boolean | true loadedAll | All messages have been loaded | boolean | `true`
emptyText | message text when list is empty | ReactNode | - loading | Loading status of this tab | boolean | `false`
emptyImage | image when list is empty | string | - 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 ### Tab data
Property | Description | Type | Default Property | Description | Type | Default
----|------|-----|------ ----|------|-----|------
avatar | avatar img url | string \| ReactNode | - avatar | avatar img url | string \| ReactNode | -
title | title | ReactNode | - title | title | ReactNode | -
description | description info | ReactNode | - description | description info | ReactNode | -
datetime | Timestamps | 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`
...@@ -21,6 +21,8 @@ export default class NoticeIcon extends PureComponent { ...@@ -21,6 +21,8 @@ export default class NoticeIcon extends PureComponent {
locale: { locale: {
emptyText: 'No notifications', emptyText: 'No notifications',
clear: 'Clear', clear: 'Clear',
loadedAll: 'Loaded',
loadMore: 'Loading more',
}, },
emptyImage: 'https://gw.alipayobjects.com/zos/rmsportal/wAhyIChODzsoKIOBHcBk.svg', emptyImage: 'https://gw.alipayobjects.com/zos/rmsportal/wAhyIChODzsoKIOBHcBk.svg',
}; };
...@@ -51,25 +53,53 @@ export default class NoticeIcon extends PureComponent { ...@@ -51,25 +53,53 @@ export default class NoticeIcon extends PureComponent {
onTabChange(tabType); onTabChange(tabType);
}; };
onLoadMore = (tabProps, event) => {
const { onLoadMore } = this.props;
onLoadMore(tabProps, event);
};
getNotificationBox() { getNotificationBox() {
const { visible } = this.state;
const { children, loading, locale } = this.props; const { children, loading, locale } = this.props;
if (!children) { if (!children) {
return null; return null;
} }
const panes = React.Children.map(children, child => { 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 len = list && list.length ? list.length : 0;
const msgCount = count || count === 0 ? count : len; const msgCount = count || count === 0 ? count : len;
const tabTitle = msgCount > 0 ? `${title} (${msgCount})` : title; const tabTitle = msgCount > 0 ? `${title} (${msgCount})` : title;
return ( return (
<TabPane tab={tabTitle} key={name}> <TabPane tab={tabTitle} key={name}>
<List <List
{...child.props}
data={list} data={list}
onClick={item => this.onItemClick(item, child.props)} emptyImage={emptyImage}
emptyText={emptyText}
loadedAll={loadedAll}
loading={tabLoading}
locale={locale}
onClear={() => this.onClear(name)} 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} title={title}
locale={locale} visible={visible}
/> />
</TabPane> </TabPane>
); );
......
...@@ -20,7 +20,7 @@ ...@@ -20,7 +20,7 @@
text-align: center; text-align: center;
} }
.ant-tabs-bar { .ant-tabs-bar {
margin-bottom: 4px; margin-bottom: 0;
} }
} }
} }
...@@ -13,26 +13,32 @@ order: 9 ...@@ -13,26 +13,32 @@ order: 9
----|------|-----|------ ----|------|-----|------
count | 图标上的消息总数 | number | - count | 图标上的消息总数 | number | -
bell | translate this please -> Change the bell Icon | ReactNode | `<Icon type='bell' />` bell | translate this please -> Change the bell Icon | ReactNode | `<Icon type='bell' />`
loading | 弹出卡片加载状态 | boolean | false loading | 弹出卡片加载状态 | boolean | `false`
onClear | 点击清空按钮的回调 | function(tabName) | - onClear | 点击清空按钮的回调 | function(tabName) | -
onItemClick | 点击列表项的回调 | function(item, tabProps) | - onItemClick | 点击列表项的回调 | function(item, tabProps) | -
onTabChange | 切换页签的回调 | function(tabTitle) | - onLoadMore | 加载更多的回调 | function(tabProps, event) | -
onPopupVisibleChange | 弹出卡片显隐的回调 | function(visible) | - onPopupVisibleChange | 弹出卡片显隐的回调 | function(visible) | -
onTabChange | 切换页签的回调 | function(tabTitle) | -
popupVisible | 控制弹层显隐 | boolean | - popupVisible | 控制弹层显隐 | boolean | -
locale | 默认文案 | Object | `{ emptyText: '暂无数据', clear: '清空' }` locale | 默认文案 | Object | `{ emptyText: 'No notifications', clear: 'Clear', loadedAll: 'Loaded', loadMore: 'Loading more' }`
clearClose | 点击清空按钮后关闭通知菜单 | boolean | false clearClose | 点击清空按钮后关闭通知菜单 | boolean | `false`
### NoticeIcon.Tab ### NoticeIcon.Tab
参数 | 说明 | 类型 | 默认值 参数 | 说明 | 类型 | 默认值
----|------|-----|------ ----|------|-----|------
title | 消息分类的页签标题 | string | - count | 当前 Tab 未读消息数量 | number | list.length
name | 消息分类的标识符 | string | -
list | 列表数据,格式参照下表 | Array | `[]`
showClear | 是否显示清空按钮 | boolean | true
emptyText | 针对每个 Tab 定制空数据文案 | ReactNode | - emptyText | 针对每个 Tab 定制空数据文案 | ReactNode | -
emptyImage | 针对每个 Tab 定制空数据图片 | string | - 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 ### Tab data
...@@ -43,4 +49,4 @@ title | 标题 | ReactNode | - ...@@ -43,4 +49,4 @@ title | 标题 | ReactNode | -
description | 描述信息 | ReactNode | - description | 描述信息 | ReactNode | -
datetime | 时间戳 | ReactNode | - datetime | 时间戳 | ReactNode | -
extra | 额外信息,在列表项右上角 | ReactNode | - extra | 额外信息,在列表项右上角 | ReactNode | -
clickClose | 点击列表项关闭通知菜单 | boolean | false clickClose | 点击列表项关闭通知菜单 | boolean | `false`
...@@ -153,7 +153,9 @@ class HeaderView extends PureComponent { ...@@ -153,7 +153,9 @@ class HeaderView extends PureComponent {
export default connect(({ user, global, setting, loading }) => ({ export default connect(({ user, global, setting, loading }) => ({
currentUser: user.currentUser, currentUser: user.currentUser,
collapsed: global.collapsed, collapsed: global.collapsed,
fetchingMoreNotices: loading.effects['global/fetchMoreNotices'],
fetchingNotices: loading.effects['global/fetchNotices'], fetchingNotices: loading.effects['global/fetchNotices'],
loadedAllNotices: global.loadedAllNotices,
notices: global.notices, notices: global.notices,
setting, setting,
}))(HeaderView); }))(HeaderView);
...@@ -13,4 +13,6 @@ export default { ...@@ -13,4 +13,6 @@ export default {
'component.noticeIcon.clear': 'Clear', 'component.noticeIcon.clear': 'Clear',
'component.noticeIcon.cleared': 'Cleared', 'component.noticeIcon.cleared': 'Cleared',
'component.noticeIcon.empty': 'No notifications', 'component.noticeIcon.empty': 'No notifications',
'component.noticeIcon.loaded': 'Loaded',
'component.noticeIcon.loading-more': 'Loading more',
}; };
...@@ -13,4 +13,6 @@ export default { ...@@ -13,4 +13,6 @@ export default {
'component.noticeIcon.clear': 'Limpar', 'component.noticeIcon.clear': 'Limpar',
'component.noticeIcon.cleared': 'Limpo', 'component.noticeIcon.cleared': 'Limpo',
'component.noticeIcon.empty': 'Sem notificações', 'component.noticeIcon.empty': 'Sem notificações',
'component.noticeIcon.loaded': 'Carregado',
'component.noticeIcon.loading-more': 'Carregar mais',
}; };
...@@ -13,4 +13,6 @@ export default { ...@@ -13,4 +13,6 @@ export default {
'component.noticeIcon.clear': '清空', 'component.noticeIcon.clear': '清空',
'component.noticeIcon.cleared': '清空了', 'component.noticeIcon.cleared': '清空了',
'component.noticeIcon.empty': '暂无数据', 'component.noticeIcon.empty': '暂无数据',
'component.noticeIcon.loaded': '加载完毕',
'component.noticeIcon.loading-more': '加载更多',
}; };
...@@ -13,4 +13,6 @@ export default { ...@@ -13,4 +13,6 @@ export default {
'component.noticeIcon.clear': '清空', 'component.noticeIcon.clear': '清空',
'component.noticeIcon.cleared': '清空了', 'component.noticeIcon.cleared': '清空了',
'component.noticeIcon.empty': '暫無數據', 'component.noticeIcon.empty': '暫無數據',
'component.noticeIcon.loaded': '加載完畢',
'component.noticeIcon.loading-more': '加載更多',
}; };
...@@ -6,14 +6,42 @@ export default { ...@@ -6,14 +6,42 @@ export default {
state: { state: {
collapsed: false, collapsed: false,
notices: [], notices: [],
loadedAllNotices: false,
}, },
effects: { effects: {
*fetchNotices(_, { call, put, select }) { *fetchNotices(_, { call, put, select }) {
const data = yield call(queryNotices); const data = yield call(queryNotices);
const loadedAllNotices = data && data.length && data[data.length - 1] === null;
yield put({
type: 'setLoadedStatus',
payload: loadedAllNotices,
});
yield put({ yield put({
type: 'saveNotices', type: 'saveNotices',
payload: data, payload: data.filter(item => item),
});
const unreadCount = yield select(
state => state.global.notices.filter(item => !item.read).length
);
yield put({
type: 'user/changeNotifyCount',
payload: {
totalCount: data.length,
unreadCount,
},
});
},
*fetchMoreNotices({ payload }, { call, put, select }) {
const data = yield call(queryNotices, payload);
const loadedAllNotices = data && data.length && data[data.length - 1] === null;
yield put({
type: 'setLoadedStatus',
payload: loadedAllNotices,
});
yield put({
type: 'pushNotices',
payload: data.filter(item => item),
}); });
const unreadCount = yield select( const unreadCount = yield select(
state => state.global.notices.filter(item => !item.read).length state => state.global.notices.filter(item => !item.read).length
...@@ -86,6 +114,18 @@ export default { ...@@ -86,6 +114,18 @@ export default {
notices: state.notices.filter(item => item.type !== payload), notices: state.notices.filter(item => item.type !== payload),
}; };
}, },
pushNotices(state, { payload }) {
return {
...state,
notices: [...state.notices, ...payload],
};
},
setLoadedStatus(state, { payload }) {
return {
...state,
loadedAllNotices: payload,
};
},
}, },
subscriptions: { subscriptions: {
......
...@@ -117,8 +117,8 @@ export async function fakeRegister(params) { ...@@ -117,8 +117,8 @@ export async function fakeRegister(params) {
}); });
} }
export async function queryNotices() { export async function queryNotices(params = {}) {
return request('/api/notices'); return request(`/api/notices?${stringify(params)}`);
} }
export async function getFakeCaptcha(mobile) { export async function getFakeCaptcha(mobile) {
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment