Преглед изворни кода

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
何乐 пре 7 година
родитељ
комит
24e01da4e0

+ 111 - 98
mock/notices.js

@@ -1,101 +1,114 @@
-const getNotices = (req, res) =>
-  res.json([
-    {
-      id: '000000001',
-      avatar: 'https://gw.alipayobjects.com/zos/rmsportal/ThXAXghbEsBCCSDihZxY.png',
-      title: '你收到了 14 份新周报',
-      datetime: '2017-08-09',
-      type: 'notification',
-    },
-    {
-      id: '000000002',
-      avatar: 'https://gw.alipayobjects.com/zos/rmsportal/OKJXDXrmkNshAMvwtvhu.png',
-      title: '你推荐的 曲妮妮 已通过第三轮面试',
-      datetime: '2017-08-08',
-      type: 'notification',
-    },
-    {
-      id: '000000003',
-      avatar: 'https://gw.alipayobjects.com/zos/rmsportal/kISTdvpyTAhtGxpovNWd.png',
-      title: '这种模板可以区分多种通知类型',
-      datetime: '2017-08-07',
-      read: true,
-      type: 'notification',
-    },
-    {
-      id: '000000004',
-      avatar: 'https://gw.alipayobjects.com/zos/rmsportal/GvqBnKhFgObvnSGkDsje.png',
-      title: '左侧图标用于区分不同的类型',
-      datetime: '2017-08-07',
-      type: 'notification',
-    },
-    {
-      id: '000000005',
-      avatar: 'https://gw.alipayobjects.com/zos/rmsportal/ThXAXghbEsBCCSDihZxY.png',
-      title: '内容不要超过两行字,超出时自动截断',
-      datetime: '2017-08-07',
-      type: 'notification',
-    },
-    {
-      id: '000000006',
-      avatar: 'https://gw.alipayobjects.com/zos/rmsportal/fcHMVNCjPOsbUGdEduuv.jpeg',
-      title: '曲丽丽 评论了你',
-      description: '描述信息描述信息描述信息',
-      datetime: '2017-08-07',
-      type: 'message',
-      clickClose: true,
-    },
-    {
-      id: '000000007',
-      avatar: 'https://gw.alipayobjects.com/zos/rmsportal/fcHMVNCjPOsbUGdEduuv.jpeg',
-      title: '朱偏右 回复了你',
-      description: '这种模板用于提醒谁与你发生了互动,左侧放『谁』的头像',
-      datetime: '2017-08-07',
-      type: 'message',
-      clickClose: true,
-    },
-    {
-      id: '000000008',
-      avatar: 'https://gw.alipayobjects.com/zos/rmsportal/fcHMVNCjPOsbUGdEduuv.jpeg',
-      title: '标题',
-      description: '这种模板用于提醒谁与你发生了互动,左侧放『谁』的头像',
-      datetime: '2017-08-07',
-      type: 'message',
-      clickClose: true,
-    },
-    {
-      id: '000000009',
-      title: '任务名称',
-      description: '任务需要在 2017-01-12 20:00 前启动',
-      extra: '未开始',
-      status: 'todo',
-      type: 'event',
-    },
-    {
-      id: '000000010',
-      title: '第三方紧急代码变更',
-      description: '冠霖提交于 2017-01-06,需在 2017-01-07 前完成代码变更任务',
-      extra: '马上到期',
-      status: 'urgent',
-      type: 'event',
-    },
-    {
-      id: '000000011',
-      title: '信息安全考试',
-      description: '指派竹尔于 2017-01-09 前完成更新并发布',
-      extra: '已耗时 8 天',
-      status: 'doing',
-      type: 'event',
-    },
-    {
-      id: '000000012',
-      title: 'ABCD 版本发布',
-      description: '冠霖提交于 2017-01-06,需在 2017-01-07 前完成代码变更任务',
-      extra: '进行中',
-      status: 'processing',
-      type: 'event',
-    },
-  ]);
+const fakeNotices = [
+  {
+    id: '000000001',
+    avatar: 'https://gw.alipayobjects.com/zos/rmsportal/ThXAXghbEsBCCSDihZxY.png',
+    title: '你收到了 14 份新周报',
+    datetime: '2017-08-09',
+    type: 'notification',
+  },
+  {
+    id: '000000002',
+    avatar: 'https://gw.alipayobjects.com/zos/rmsportal/OKJXDXrmkNshAMvwtvhu.png',
+    title: '你推荐的 曲妮妮 已通过第三轮面试',
+    datetime: '2017-08-08',
+    type: 'notification',
+  },
+  {
+    id: '000000003',
+    avatar: 'https://gw.alipayobjects.com/zos/rmsportal/kISTdvpyTAhtGxpovNWd.png',
+    title: '这种模板可以区分多种通知类型',
+    datetime: '2017-08-07',
+    read: true,
+    type: 'notification',
+  },
+  {
+    id: '000000004',
+    avatar: 'https://gw.alipayobjects.com/zos/rmsportal/GvqBnKhFgObvnSGkDsje.png',
+    title: '左侧图标用于区分不同的类型',
+    datetime: '2017-08-07',
+    type: 'notification',
+  },
+  {
+    id: '000000005',
+    avatar: 'https://gw.alipayobjects.com/zos/rmsportal/ThXAXghbEsBCCSDihZxY.png',
+    title: '内容不要超过两行字,超出时自动截断',
+    datetime: '2017-08-07',
+    type: 'notification',
+  },
+  {
+    id: '000000006',
+    avatar: 'https://gw.alipayobjects.com/zos/rmsportal/fcHMVNCjPOsbUGdEduuv.jpeg',
+    title: '曲丽丽 评论了你',
+    description: '描述信息描述信息描述信息',
+    datetime: '2017-08-07',
+    type: 'message',
+    clickClose: true,
+  },
+  {
+    id: '000000007',
+    avatar: 'https://gw.alipayobjects.com/zos/rmsportal/fcHMVNCjPOsbUGdEduuv.jpeg',
+    title: '朱偏右 回复了你',
+    description: '这种模板用于提醒谁与你发生了互动,左侧放『谁』的头像',
+    datetime: '2017-08-07',
+    type: 'message',
+    clickClose: true,
+  },
+  {
+    id: '000000008',
+    avatar: 'https://gw.alipayobjects.com/zos/rmsportal/fcHMVNCjPOsbUGdEduuv.jpeg',
+    title: '标题',
+    description: '这种模板用于提醒谁与你发生了互动,左侧放『谁』的头像',
+    datetime: '2017-08-07',
+    type: 'message',
+    clickClose: true,
+  },
+  {
+    id: '000000009',
+    title: '任务名称',
+    description: '任务需要在 2017-01-12 20:00 前启动',
+    extra: '未开始',
+    status: 'todo',
+    type: 'event',
+  },
+  {
+    id: '000000010',
+    title: '第三方紧急代码变更',
+    description: '冠霖提交于 2017-01-06,需在 2017-01-07 前完成代码变更任务',
+    extra: '马上到期',
+    status: 'urgent',
+    type: 'event',
+  },
+  {
+    id: '000000011',
+    title: '信息安全考试',
+    description: '指派竹尔于 2017-01-09 前完成更新并发布',
+    extra: '已耗时 8 天',
+    status: 'doing',
+    type: 'event',
+  },
+  {
+    id: '000000012',
+    title: 'ABCD 版本发布',
+    description: '冠霖提交于 2017-01-06,需在 2017-01-07 前完成代码变更任务',
+    extra: '进行中',
+    status: 'processing',
+    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 {
   'GET /api/notices': getNotices,

+ 28 - 0
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 = (
@@ -93,6 +110,11 @@ export default class GlobalHeaderRight extends PureComponent {
         </Menu.Item>
       </Menu>
     );
+    const loadMoreProps = {
+      skeletonCount,
+      loadedAll: loadedAllNotices,
+      loading: fetchingMoreNotices,
+    };
     const noticeData = this.getNoticeData();
     const unreadMsg = this.getUnreadData(noticeData);
     let className = styles.right;
@@ -136,8 +158,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
@@ -149,6 +174,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}
           />
           <NoticeIcon.Tab
             count={unreadMsg.message}
@@ -157,6 +183,7 @@ export default class GlobalHeaderRight extends PureComponent {
             name="message"
             emptyText={formatMessage({ id: 'component.globalHeader.message.empty' })}
             emptyImage="https://gw.alipayobjects.com/zos/rmsportal/sAuJeJzSKbUmHfBQRzmZ.svg"
+            {...loadMoreProps}
           />
           <NoticeIcon.Tab
             count={unreadMsg.event}
@@ -165,6 +192,7 @@ export default class GlobalHeaderRight extends PureComponent {
             name="event"
             emptyText={formatMessage({ id: 'component.globalHeader.event.empty' })}
             emptyImage="https://gw.alipayobjects.com/zos/rmsportal/HsIsxMZiWKrNUavQUXqx.svg"
+            {...loadMoreProps}
           />
         </NoticeIcon>
         {currentUser.name ? (

+ 10 - 4
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<INoticeIconTabProps, any> {}

+ 58 - 21
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({
       </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 (
     <div>
-      <List className={styles.list}>
-        {data.map((item, i) => {
+      <List className={styles.list} loadMore={LoadMore} onScroll={onScroll}>
+        {[...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' ? (
               <Avatar className={styles.avatar} src={item.avatar} />
             ) : (
-              item.avatar
+              <span className={styles.iconElement}>{item.avatar}</span>
             )
           ) : null;
 
           return (
             <List.Item className={itemCls} key={item.key || i} onClick={() => onClick(item)}>
-              <List.Item.Meta
-                className={styles.meta}
-                avatar={<span className={styles.iconElement}>{leftIcon}</span>}
-                title={
-                  <div className={styles.title}>
-                    {item.title}
-                    <div className={styles.extra}>{item.extra}</div>
-                  </div>
-                }
-                description={
-                  <div>
-                    <div className={styles.description} title={item.description}>
-                      {item.description}
+              <Skeleton avatar title={false} active {...skeletonProps} loading={item.loading}>
+                <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} title={item.description}>
+                        {item.description}
+                      </div>
+                      <div className={styles.datetime}>{item.datetime}</div>
                     </div>
-                    <div className={styles.datetime}>{item.datetime}</div>
-                  </div>
-                }
-              />
+                  }
+                />
+              </Skeleton>
             </List.Item>
           );
         })}

+ 13 - 0
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 {

+ 7 - 1
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;
 }
 

+ 20 - 12
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 | `<Icon type='bell' />`
-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`

+ 34 - 4
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 (
         <TabPane tab={tabTitle} key={name}>
           <List
-            {...child.props}
             data={list}
-            onClick={item => this.onItemClick(item, child.props)}
+            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}
-            locale={locale}
+            visible={visible}
           />
         </TabPane>
       );

+ 1 - 1
src/components/NoticeIcon/index.less

@@ -20,7 +20,7 @@
       text-align: center;
     }
     .ant-tabs-bar {
-      margin-bottom: 4px;
+      margin-bottom: 0;
     }
   }
 }

+ 16 - 10
src/components/NoticeIcon/index.zh-CN.md

@@ -13,26 +13,32 @@ order: 9
 ----|------|-----|------
 count | 图标上的消息总数 | number | -
 bell | translate this please -> Change the bell Icon | ReactNode | `<Icon type='bell' />`
-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`

+ 2 - 0
src/layouts/Header.js

@@ -153,7 +153,9 @@ class HeaderView extends PureComponent {
 export default connect(({ user, global, setting, loading }) => ({
   currentUser: user.currentUser,
   collapsed: global.collapsed,
+  fetchingMoreNotices: loading.effects['global/fetchMoreNotices'],
   fetchingNotices: loading.effects['global/fetchNotices'],
+  loadedAllNotices: global.loadedAllNotices,
   notices: global.notices,
   setting,
 }))(HeaderView);

+ 2 - 0
src/locales/en-US/globalHeader.js

@@ -13,4 +13,6 @@ export default {
   'component.noticeIcon.clear': 'Clear',
   'component.noticeIcon.cleared': 'Cleared',
   'component.noticeIcon.empty': 'No notifications',
+  'component.noticeIcon.loaded': 'Loaded',
+  'component.noticeIcon.loading-more': 'Loading more',
 };

+ 2 - 0
src/locales/pt-BR/globalHeader.js

@@ -13,4 +13,6 @@ export default {
   'component.noticeIcon.clear': 'Limpar',
   'component.noticeIcon.cleared': 'Limpo',
   'component.noticeIcon.empty': 'Sem notificações',
+  'component.noticeIcon.loaded': 'Carregado',
+  'component.noticeIcon.loading-more': 'Carregar mais',
 };

+ 2 - 0
src/locales/zh-CN/globalHeader.js

@@ -13,4 +13,6 @@ export default {
   'component.noticeIcon.clear': '清空',
   'component.noticeIcon.cleared': '清空了',
   'component.noticeIcon.empty': '暂无数据',
+  'component.noticeIcon.loaded': '加载完毕',
+  'component.noticeIcon.loading-more': '加载更多',
 };

+ 2 - 0
src/locales/zh-TW/globalHeader.js

@@ -13,4 +13,6 @@ export default {
   'component.noticeIcon.clear': '清空',
   'component.noticeIcon.cleared': '清空了',
   'component.noticeIcon.empty': '暫無數據',
+  'component.noticeIcon.loaded': '加載完畢',
+  'component.noticeIcon.loading-more': '加載更多',
 };

+ 41 - 1
src/models/global.js

@@ -6,14 +6,42 @@ export default {
   state: {
     collapsed: false,
     notices: [],
+    loadedAllNotices: false,
   },
 
   effects: {
     *fetchNotices(_, { call, put, select }) {
       const data = yield call(queryNotices);
+      const loadedAllNotices = data && data.length && data[data.length - 1] === null;
+      yield put({
+        type: 'setLoadedStatus',
+        payload: loadedAllNotices,
+      });
       yield put({
         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(
         state => state.global.notices.filter(item => !item.read).length
@@ -86,6 +114,18 @@ export default {
         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: {

+ 2 - 2
src/services/api.js

@@ -117,8 +117,8 @@ export async function fakeRegister(params) {
   });
 }
 
-export async function queryNotices() {
-  return request('/api/notices');
+export async function queryNotices(params = {}) {
+  return request(`/api/notices?${stringify(params)}`);
 }
 
 export async function getFakeCaptcha(mobile) {