Sfoglia il codice sorgente

refactor: 通知组件重构

lanjianrong 5 anni fa
parent
commit
5c881f11a5

+ 126 - 0
src/components/RightContent/NoticeIcon/NoticeIcon.tsx

@@ -0,0 +1,126 @@
+import { BellOutlined } from '@ant-design/icons'
+import { Badge, Spin, Tabs } from 'antd'
+import useMergedState from 'rc-util/es/hooks/useMergedState'
+import React from 'react'
+import classNames from 'classnames'
+import type { NoticeIconTabProps } from './NoticeList'
+import NoticeList from './NoticeList'
+import styles from './index.less'
+import HeaderDropdown from '@/components/HeaderDropdown'
+
+const { TabPane } = Tabs
+
+export type NoticeIconProps = {
+  count?: number
+  bell?: React.ReactNode
+  className?: string
+  loading?: boolean
+  onClear?: (tabName: string, tabKey: string) => void
+  onItemClick?: (item: API.NoticeIconItem, 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
+  emptyImage?: string
+  children?: React.ReactElement<NoticeIconTabProps>[]
+}
+
+const NoticeIcon: React.FC<NoticeIconProps> & {
+  Tab: typeof NoticeList
+} = props => {
+  const getNotificationBox = (): React.ReactNode => {
+    const {
+      children,
+      loading,
+      onClear,
+      onTabChange,
+      onItemClick,
+      onViewMore,
+      clearText,
+      viewMoreText
+    } = props
+    if (!children) {
+      return null
+    }
+    const panes: React.ReactNode[] = []
+    React.Children.forEach(children, (child: React.ReactElement<NoticeIconTabProps>): void => {
+      if (!child) {
+        return
+      }
+      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
+      panes.push(
+        <TabPane tab={tabTitle} key={tabKey}>
+          <NoticeList
+            clearText={clearText}
+            viewMoreText={viewMoreText}
+            list={list}
+            tabKey={tabKey}
+            onClear={(): void => onClear && onClear(title, tabKey)}
+            onClick={(item): void => onItemClick && onItemClick(item, child.props)}
+            onViewMore={(event): void => onViewMore && onViewMore(child.props, event)}
+            showClear={showClear}
+            showViewMore={showViewMore}
+            title={title}
+          />
+        </TabPane>
+      )
+    })
+    return (
+      <>
+        <Spin spinning={loading} delay={300}>
+          <Tabs className={styles.tabs} onChange={onTabChange}>
+            {panes}
+          </Tabs>
+        </Spin>
+      </>
+    )
+  }
+
+  const { className, count, bell } = props
+
+  const [visible, setVisible] = useMergedState<boolean>(false, {
+    value: props.popupVisible,
+    onChange: props.onPopupVisibleChange
+  })
+  const noticeButtonClass = classNames(className, styles.noticeButton)
+  const notificationBox = getNotificationBox()
+  const NoticeBellIcon = bell || <BellOutlined 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
+  }
+
+  return (
+    <HeaderDropdown
+      placement="bottomRight"
+      overlay={notificationBox}
+      overlayClassName={styles.popover}
+      trigger={['click']}
+      visible={visible}
+      onVisibleChange={setVisible}
+    >
+      {trigger}
+    </HeaderDropdown>
+  )
+}
+
+NoticeIcon.defaultProps = {
+  emptyImage: 'https://gw.alipayobjects.com/zos/rmsportal/wAhyIChODzsoKIOBHcBk.svg'
+}
+
+NoticeIcon.Tab = NoticeList
+
+export default NoticeIcon

+ 103 - 0
src/components/RightContent/NoticeIcon/NoticeList.less

@@ -0,0 +1,103 @@
+@import '~antd/es/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: @component-background;
+    }
+    .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;
+  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;
+
+    &:only-child {
+      width: 100%;
+    }
+    &:not(:only-child):last-child {
+      border-left: 1px solid @border-color-split;
+    }
+  }
+}

+ 113 - 0
src/components/RightContent/NoticeIcon/NoticeList.tsx

@@ -0,0 +1,113 @@
+import { Avatar, List } from 'antd'
+
+import React from 'react'
+import classNames from 'classnames'
+import styles from './NoticeList.less'
+
+export type NoticeIconTabProps = {
+  count?: number
+  showClear?: boolean
+  showViewMore?: boolean
+  style?: React.CSSProperties
+  title: string
+  tabKey: API.NoticeIconItemType
+  onClick?: (item: API.NoticeIconItem) => void
+  onClear?: () => void
+  emptyText?: string
+  clearText?: string
+  viewMoreText?: string
+  list: API.NoticeIconItem[]
+  onViewMore?: (e: any) => void
+}
+const NoticeList: React.FC<NoticeIconTabProps> = ({
+  list = [],
+  onClick,
+  onClear,
+  title,
+  onViewMore,
+  emptyText,
+  showClear = true,
+  clearText,
+  viewMoreText,
+  showViewMore = false
+}) => {
+  if (!list || list.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
+        className={styles.list}
+        dataSource={list}
+        renderItem={(item, i) => {
+          const itemCls = classNames(styles.item, {
+            [styles.read]: item.isRead
+          })
+          // 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?.(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.createTime}>{item.createTime}</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

+ 35 - 0
src/components/RightContent/NoticeIcon/index.less

@@ -0,0 +1,35 @@
+@import '~antd/es/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-list {
+      margin: auto;
+    }
+
+    .ant-tabs-nav-scroll {
+      text-align: center;
+    }
+    .ant-tabs-bar {
+      margin-bottom: 0;
+    }
+  }
+}

+ 158 - 0
src/components/RightContent/NoticeIcon/index.tsx

@@ -0,0 +1,158 @@
+import { useCallback, useEffect, useState } from 'react'
+import { Tag, message } from 'antd'
+import { groupBy } from 'lodash'
+import { useModel } from 'umi'
+
+import NoticeIcon from './NoticeIcon'
+import styles from './index.less'
+import dayjs from 'dayjs'
+import { queryNoticeList } from '@/services/login'
+import consts from '@/consts'
+
+export type GlobalHeaderRightProps = {
+  fetchingNotices?: boolean
+  onNoticeVisibleChange?: (visible: boolean) => void
+  onNoticeClear?: (tabName?: string) => void
+}
+
+const getNoticeData = notices => {
+  if (!notices || notices.length === 0 || !Array.isArray(notices)) {
+    return {}
+  }
+
+  const newNotices = notices.map(notice => {
+    const newNotice = { ...notice }
+
+    if (newNotice.datetime) {
+      newNotice.createTime = dayjs(notice.datetime as string).diff(dayjs(), 'date')
+    }
+
+    if (newNotice.id) {
+      newNotice.key = newNotice.id
+    }
+
+    if (newNotice.extra && newNotice.status) {
+      const color = {
+        todo: '',
+        processing: 'blue',
+        urgent: 'red',
+        doing: 'gold'
+      }[newNotice.status]
+      newNotice.extra = (
+        <Tag
+          color={color}
+          style={{
+            marginRight: 0
+          }}
+        >
+          {newNotice.extra}
+        </Tag>
+      ) as any
+    }
+
+    return newNotice
+  })
+  return groupBy(newNotices, 'msgType')
+}
+
+const getUnreadData = (noticeData: Record<string, API.NoticeIconItem[]>) => {
+  const unreadMsg: Record<string, number> = {}
+  Object.keys(noticeData).forEach(key => {
+    const value = noticeData[key]
+
+    if (!unreadMsg[key]) {
+      unreadMsg[key] = 0
+    }
+
+    if (Array.isArray(value)) {
+      unreadMsg[key] = value.filter(item => !item.read).length
+    }
+  })
+  return unreadMsg
+}
+
+const NoticeIconView = () => {
+  const { initialState } = useModel('@@initialState')
+  const { currentUser } = initialState || {}
+  const [notices, setNotices] = useState([])
+  const getNotices = useCallback(async () => {
+    const { code = -1, data } = await queryNoticeList()
+    if (code === consts.RET_CODE.SUCCESS) {
+      setNotices(data.message)
+    }
+  }, [])
+  useEffect(() => {
+    getNotices()
+  }, [])
+
+  const noticeData = getNoticeData(notices)
+  const unreadMsg = getUnreadData(noticeData || {})
+
+  const changeReadState = (id: string) => {
+    setNotices(
+      notices.map(item => {
+        const notice = { ...item }
+        if (notice.id === id) {
+          notice.isRead = true
+        }
+        return notice
+      })
+    )
+  }
+
+  const clearReadState = (title: string, key: string) => {
+    setNotices(
+      notices.map(item => {
+        const notice = { ...item }
+        if (notice.msgType === key) {
+          notice.isRead = true
+        }
+        return notice
+      })
+    )
+    message.success(`${'清空了'} ${title}`)
+  }
+
+  return (
+    <NoticeIcon
+      className={styles.action}
+      count={currentUser && currentUser.unreadCount}
+      onItemClick={item => {
+        changeReadState(item.id!)
+      }}
+      onClear={(title: string, key: string) => clearReadState(title, key)}
+      loading={false}
+      clearText="清空"
+      viewMoreText="查看更多"
+      onViewMore={() => message.info('Click on view more')}
+      clearClose
+    >
+      <NoticeIcon.Tab
+        tabKey="notification"
+        count={unreadMsg.notification}
+        list={noticeData.notification}
+        title="通知"
+        emptyText="你已查看所有通知"
+        showViewMore
+      />
+      <NoticeIcon.Tab
+        tabKey="message"
+        count={unreadMsg.message}
+        list={noticeData.message}
+        title="消息"
+        emptyText="您已读完所有消息"
+        showViewMore
+      />
+      <NoticeIcon.Tab
+        tabKey="event"
+        title="待办"
+        emptyText="你已完成所有待办"
+        count={unreadMsg.event}
+        list={noticeData.event}
+        showViewMore
+      />
+    </NoticeIcon>
+  )
+}
+
+export default NoticeIconView

+ 16 - 6
src/components/RightContent/NotifyDropdown.tsx

@@ -1,7 +1,7 @@
 import consts from '@/consts'
 import { queryNoticeList, queryUnreadNotice, updateNotice } from '@/services/login'
 import { BellOutlined } from '@ant-design/icons'
-import { Badge, Tabs, List, Typography, Popover } from 'antd'
+import { Badge, Tabs, List, Typography, Popover, Tag } from 'antd'
 import { useState, useCallback, useEffect } from 'react'
 import styles from './index.less'
 
@@ -47,7 +47,7 @@ const DropdownContent = ({ noticeCount }) => {
         key="1"
         tab={
           <div>
-            通知
+            系统信息
             {noticeCount ? <span> ({noticeCount}) </span> : null}
           </div>
         }
@@ -58,11 +58,21 @@ const DropdownContent = ({ noticeCount }) => {
           renderItem={item => (
             <List.Item>
               <List.Item.Meta
-                description={item.createTime}
+                // avatar={item.title}
+                description={
+                  <div className="flex justify-between items-center">
+                    <span>
+                      <Tag>{item.title}</Tag>
+                    </span>
+                    <span>{item.createTime}</span>
+                  </div>
+                }
                 title={
                   <Typography.Paragraph
                     onClick={() => handleTitleClick(item)}
-                    ellipsis={{ rows: 1, tooltip: true }}
+                    ellipsis={{
+                      rows: 2
+                    }}
                     delete={!!item.isRead}
                     style={{
                       cursor: !item.isRead ? 'pointer' : '',
@@ -78,8 +88,8 @@ const DropdownContent = ({ noticeCount }) => {
           )}
         />
       </Tabs.TabPane>
-      <Tabs.TabPane tab="消息" key="2" />
-      <Tabs.TabPane tab="代办" key="3" />
+      <Tabs.TabPane tab="消息通知" key="2" />
+      <Tabs.TabPane tab="代办事项" key="3" />
     </Tabs>
   )
 }

+ 1 - 1
src/components/RightContent/index.tsx

@@ -4,7 +4,7 @@ import { Tag, Space } from 'antd'
 import React from 'react'
 import { useModel, SelectLang } from 'umi'
 import Avatar from './AvatarDropdown'
-import Notify from './NotifyDropdown'
+import Notify from './NoticeIcon'
 // import HeaderSearch from '../HeaderSearch'
 import styles from './index.less'