陈帅 6 лет назад
Родитель
Сommit
56a5bcf5f3

+ 0 - 1
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",

+ 41 - 31
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<GlobalHeaderRightProps> {
+class GlobalHeaderRight extends Component<GlobalHeaderRightProps> {
   getNoticeData = (): { [key: string]: NoticeItem[] } => {
     const { notices = [] } = this.props;
     if (notices.length === 0) {
@@ -78,16 +79,24 @@ export default class GlobalHeaderRight extends Component<GlobalHeaderRightProps>
       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 = (
       <Menu className={styles.menu} selectedKeys={[]} onClick={onMenuClick}>
         <Menu.Item key="userCenter">
@@ -146,46 +155,39 @@ export default class GlobalHeaderRight extends Component<GlobalHeaderRightProps>
         <NoticeIcon
           className={styles.action}
           count={currentUser && currentUser.unreadCount}
-          onItemClick={(item: NoticeItem, tabProps: any) => {
-            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
         >
           <NoticeIcon.Tab
+            tabKey="notification"
             count={unreadMsg.notification}
             list={noticeData.notification}
-            title="notification"
+            title={formatMessage({ id: 'component.globalHeader.notification' })}
             emptyText={formatMessage({ id: 'component.globalHeader.notification.empty' })}
-            emptyImage="https://gw.alipayobjects.com/zos/rmsportal/wAhyIChODzsoKIOBHcBk.svg"
             showViewMore
           />
           <NoticeIcon.Tab
+            tabKey="message"
             count={unreadMsg.message}
             list={noticeData.message}
-            title="message"
+            title={formatMessage({ id: 'component.globalHeader.message' })}
             emptyText={formatMessage({ id: 'component.globalHeader.message.empty' })}
-            emptyImage="https://gw.alipayobjects.com/zos/rmsportal/sAuJeJzSKbUmHfBQRzmZ.svg"
             showViewMore
           />
           <NoticeIcon.Tab
+            tabKey="event"
+            title={formatMessage({ id: 'component.globalHeader.event' })}
+            emptyText={formatMessage({ id: 'component.globalHeader.event.empty' })}
             count={unreadMsg.event}
             list={noticeData.event}
-            title="event"
-            emptyText={formatMessage({ id: 'component.globalHeader.event.empty' })}
-            emptyImage="https://gw.alipayobjects.com/zos/rmsportal/HsIsxMZiWKrNUavQUXqx.svg"
             showViewMore
           />
         </NoticeIcon>
@@ -209,3 +211,11 @@ export default class GlobalHeaderRight extends Component<GlobalHeaderRightProps>
     );
   }
 }
+
+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);

+ 105 - 0
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;
+    }
+  }
+}

+ 113 - 0
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<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;

+ 31 - 0
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;
+    }
+  }
+}

+ 168 - 0
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<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>
+    );
+  }
+}

+ 2 - 0
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<BasicLayoutProps> = props => {
         })
       }
       onChangeLayoutCollapsed={handleMenuCollapse}
+      renderRightContent={RightProps => <RightContent {...RightProps} />}
       {...props}
     >
       {children}