Browse Source

feat: Add NoticeIcon, HeaderDropdown, GlobalHeader/NoticeIconView

KevinChan 5 năm trước cách đây
mục cha
commit
be7597b4ff

+ 119 - 0
mock/notices.js

@@ -0,0 +1,119 @@
+const noticeList = [
+  {
+    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"
+  }
+];
+
+function notices(method) {
+  let res = null;
+  switch (method) {
+    case "GET":
+      res = noticeList;
+      break;
+    default:
+      res = null;
+  }
+  return res;
+}
+
+module.exports = notices;

Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 254 - 222
package-lock.json


+ 119 - 0
src/components/GlobalHeader/NoticeIconView.vue

@@ -0,0 +1,119 @@
+<template>
+  <notice-icon
+    class="action"
+    :count="unreadCount"
+    :loading="fetchingNotices"
+    :clearText="$t('message')['component.noticeIcon.clear']"
+    :viewMoreText="$t('message')['component.noticeIcon.view-more']"
+    @itemClick="handleItemClick"
+    @clear="handleNoticeClear"
+    @viewMore="handleViewMore"
+    clearClose
+  >
+    <notice-icon-tab
+      tabKey="notification"
+      :count="unreadData.notification"
+      :list="noticeData.notification"
+      :title="$t('message')['component.globalHeader.notification']"
+      :emptyText="$t('message')['component.globalHeader.notification.empty']"
+      showViewMore
+    ></notice-icon-tab>
+    <notice-icon-tab
+      tabKey="message"
+      :count="unreadData.message"
+      :list="noticeData.message"
+      :title="$t('message')['component.globalHeader.message']"
+      :emptyText="$t('message')['component.globalHeader.message.empty']"
+      showViewMore
+    ></notice-icon-tab>
+    <notice-icon-tab
+      tabKey="event"
+      :count="unreadData.event"
+      :list="noticeData.event"
+      :title="$t('message')['component.globalHeader.event']"
+      :emptyText="$t('message')['component.globalHeader.event.empty']"
+      showViewMore
+    ></notice-icon-tab>
+  </notice-icon>
+</template>
+
+<script>
+import NoticeIcon from "@/components/NoticeIcon";
+import _groupBy from "lodash/groupBy";
+import moment from "moment";
+import { mapActions, mapState } from "vuex";
+export default {
+  components: { NoticeIcon, NoticeIconTab: NoticeIcon.Tab },
+  // data(){
+  //   return {
+  //     fetchingNotices: true,
+  //   };
+  // },
+  computed: {
+    ...mapState("global", {
+      notices: state => state.notices,
+      unreadCount: state => state.unreadCount,
+      fetchingNotices: state => state.fetchingStatus.notice
+    }),
+    noticeData() {
+      const { notices = [] } = this;
+      if (notices.length === 0) {
+        return {};
+      }
+      const newNotices = notices.map(notice => {
+        const newNotice = { ...notice };
+        if (newNotice.datetime) {
+          newNotice.datetime = moment(notice.datetime).fromNow();
+        }
+        if (newNotice.id) {
+          newNotice.key = newNotice.id;
+        }
+        return newNotice;
+      });
+      return _groupBy(newNotices, "type");
+    },
+    unreadData() {
+      const unreadMsg = {};
+      Object.entries(this.noticeData).forEach(([key, value]) => {
+        if (!unreadMsg[key]) {
+          unreadMsg[key] = 0;
+        }
+        if (Array.isArray(value)) {
+          unreadMsg[key] = value.filter(item => !item.read).length;
+        }
+      });
+      return unreadMsg;
+    }
+  },
+  mounted() {
+    this.fetchNotices();
+  },
+  methods: {
+    ...mapActions("global", [
+      "fetchNotices",
+      "changeNoticeReadState",
+      "clearNotices"
+    ]),
+    handleItemClick(item) {
+      this.changeNoticeReadState(item.id);
+    },
+    handleNoticeClear(title, tabKey) {
+      this.$message.success(
+        `${this.$t("message")["component.noticeIcon.cleared"]} ${title}`
+      );
+      this.clearNotices(tabKey);
+    },
+    handleViewMore() {}
+  }
+};
+</script>
+
+<style scoped>
+.action {
+  display: inline-block;
+  height: 100%;
+  padding: 0 12px;
+  cursor: pointer;
+  transition: all 0.3s;
+}
+</style>

+ 18 - 0
src/components/HeaderDropdown/index.less

@@ -0,0 +1,18 @@
+@import "~ant-design-vue/es/style/themes/default.less";
+.header-dropdown {
+  &__container > * {
+    background-color: #fff;
+    border-radius: 4px;
+    box-shadow: @shadow-1-down;
+  }
+
+  @media screen and (max-width: @screen-xs){
+    &__container {
+      width: 100% !important;
+    }
+
+    &__container > * {
+      border-radius: 0 !important;
+    }
+  }
+}

+ 33 - 0
src/components/HeaderDropdown/index.vue

@@ -0,0 +1,33 @@
+<template>
+  <a-dropdown :overlayClassName="cls" v-bind="$attrs" v-on="$listeners">
+    <slot />
+  </a-dropdown>
+</template>
+
+<script>
+import _pickBy from "lodash/pickBy";
+import _keys from "lodash/keys";
+
+const prefixCls = "header-dropdown";
+export default {
+  name: "HeaderDropdown",
+  props: {
+    overlayClassName: String
+  },
+  computed: {
+    cls() {
+      return _keys(
+        _pickBy(
+          {
+            [`${prefixCls}__container`]: true,
+            [this.overlayClassName]: !!this.overlayClassName
+          },
+          n => n
+        )
+      ).join(" ");
+    }
+  }
+};
+</script>
+
+<style lang="less" src="./index.less"></style>

+ 32 - 0
src/components/NoticeIcon/NoticeIcon.less

@@ -0,0 +1,32 @@
+@import "~ant-design-vue/es/style/themes/default.less";
+
+.notice-icon {
+  &__notice-button {
+    display: inline-block;
+    cursor: pointer;
+    transition: all 0.3s;
+  }
+
+  &__popover {
+    position: relative;
+    width: 336px;
+  }
+  &__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;
+      }
+    }
+  }
+}

+ 131 - 0
src/components/NoticeIcon/NoticeIcon.vue

@@ -0,0 +1,131 @@
+<script>
+import HeaderDropdown from "../HeaderDropdown";
+import NoticeList from "./NoticeList";
+import PropTypes from "ant-design-vue/lib/_util/vue-types";
+import {
+  getOptionProps,
+  filterEmpty
+} from "ant-design-vue/lib/_util/props-util";
+import _map from "lodash/map";
+
+const prefixCls = "notice-icon";
+
+export default {
+  name: "NoticeIcon",
+  props: {
+    count: PropTypes.number.def(0),
+    className: PropTypes.string,
+    popupVisible: PropTypes.bool,
+    loading: PropTypes.bool,
+    clearText: PropTypes.string,
+    viewMoreText: PropTypes.string,
+    clearClose: PropTypes.bool.def(false)
+  },
+  data() {
+    return {
+      visible: false,
+      emptyImage:
+        "https://gw.alipayobjects.com/zos/rmsportal/wAhyIChODzsoKIOBHcBk.svg"
+    };
+  },
+  methods: {
+    handleVisibleChange(visible) {
+      this.visible = visible;
+    },
+    handleTabChange(tabType) {
+      this.$emit("tabChange", tabType);
+    },
+    // eslint-disable-next-line
+    getNotificationBox(h) {
+      const { loading, clearText, viewMoreText } = this;
+      const children = filterEmpty(this.$slots.default);
+      if (!children) {
+        return null;
+      }
+      const panes = _map(children, child => {
+        const childProps = getOptionProps(child);
+        const {
+          list,
+          title,
+          count,
+          tabKey,
+          showClear,
+          emptyText,
+          showViewMore
+        } = childProps;
+        const len = list && list.length ? list.length : 0;
+        const msgCount = count || count === 0 ? count : len;
+        const tabTitle = msgCount > 0 ? `${title} (${msgCount})` : title;
+        const props = {
+          clearText,
+          emptyText,
+          viewMoreText,
+          showClear,
+          showViewMore,
+          title,
+          onClick: item => this.$emit("itemClick", item, childProps),
+          onClear: () => this.$emit("clear", title, tabKey),
+          onViewMore: event => this.$emit("viewMore", childProps, event)
+        };
+        return (
+          <ATabPane tab={tabTitle} key={tabKey}>
+            <NoticeList data={list} {...{ props }} />
+          </ATabPane>
+        );
+      });
+      return (
+        <div>
+          <ASpin spinning={loading} delay={0}>
+            <ATabs class={`${prefixCls}__tabs`} onChange={this.handleTabChange}>
+              {panes}
+            </ATabs>
+          </ASpin>
+        </div>
+      );
+    }
+  },
+  render(h) {
+    const { className, count, visible, popupVisible } = this;
+    const noticeButtonClass = {
+      [className]: !!className,
+      opened: !!visible,
+      [`${prefixCls}__notice-button`]: true
+    };
+    const notificationBox = this.getNotificationBox(h);
+    const NoticeBellIcon = <a-icon type="bell" class={`${prefixCls}__icon`} />;
+    const trigger = (
+      <span class={noticeButtonClass}>
+        <ABadge
+          count={count}
+          style={{ boxShadow: "none" }}
+          class={`${prefixCls}__badge`}
+        >
+          {NoticeBellIcon}
+        </ABadge>
+      </span>
+    );
+    if (!notificationBox) {
+      return trigger;
+    }
+    const popoverProps = {};
+    if ("popupVisible" in this) {
+      popoverProps.visible = popupVisible;
+    }
+    return (
+      <HeaderDropdown
+        placement="bottomRight"
+        overlayClassName={`${prefixCls}__popover`}
+        trigger={["click"]}
+        visible={visible}
+        onVisibleChange={this.handleVisibleChange}
+        overlay={notificationBox}
+        {...popoverProps}
+      >
+        {trigger}
+      </HeaderDropdown>
+    );
+  }
+};
+</script>
+
+<style lang="less" src="./NoticeIcon.less"></style>

+ 93 - 0
src/components/NoticeIcon/NoticeList.less

@@ -0,0 +1,93 @@
+@import "~ant-design-vue/es/style/themes/default.less";
+.notice-list {
+  &__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;
+      }
+      &__icon-element {
+        font-size: 32px;
+      }
+      &__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;
+      }
+      &:last-child {
+        border-bottom: 0;
+      }
+      &:hover {
+        background: @primary-1;
+      }
+    }
+    &-item&-read {
+      opacity: 0.4;
+    }
+  }
+  &__not-found {
+    padding: 73px 0 88px 0;
+    color: @text-color-secondary;
+    text-align: center;
+    img {
+      display: inline-block;
+      height: 76px;
+      margin-bottom: 16px;
+    }
+  }
+  &__bottom-bar {
+    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;
+      }
+    }
+  }
+}

+ 144 - 0
src/components/NoticeIcon/NoticeList.vue

@@ -0,0 +1,144 @@
+<script>
+import PropTypes from "ant-design-vue/lib/_util/vue-types";
+
+const prefixCls = "notice-list";
+const genCls = moduleName => `${prefixCls}__${moduleName}`;
+
+export default {
+  name: "NoticeList",
+  // functional: true,
+  props: {
+    count: PropTypes.number,
+    list: PropTypes.array,
+    data: PropTypes.array.def([]),
+    title: PropTypes.string,
+    tabKey: PropTypes.string,
+    showClear: PropTypes.bool.def(true),
+    showViewMore: PropTypes.bool.def(false),
+    emptyText: PropTypes.string,
+    clearText: PropTypes.string,
+    viewMoreText: PropTypes.string,
+    onViewMore: PropTypes.func.def(() => null),
+    onClick: PropTypes.func.def(() => null),
+    onClear: PropTypes.func.def(() => null)
+  },
+  // eslint-disable-next-line
+  render(h) {
+    const {
+      title,
+      data,
+      emptyText,
+      clearText,
+      showClear,
+      showViewMore,
+      viewMoreText,
+      onClick,
+      onClear,
+      onViewMore
+    } = this;
+    if (data.length === 0) {
+      return (
+        <div class={genCls("not-found")}>
+          <img
+            src="https://gw.alipayobjects.com/zos/rmsportal/sAuJeJzSKbUmHfBQRzmZ.svg"
+            alt="not found"
+          />
+          <div>{emptyText}</div>
+        </div>
+      );
+    }
+    return (
+      <div>
+        <AList
+          class={genCls("list")}
+          dataSource={data}
+          renderItem={(item, i) => {
+            const itemCls = {
+              [genCls("list-item")]: true,
+              [genCls("list-read")]: !!item.read
+            };
+            const leftIcon = item.avatar ? (
+              typeof item.avatar === "string" ? (
+                <AAvatar
+                  class={genCls("list-item__avatar")}
+                  src={item.avatar}
+                />
+              ) : (
+                <span class={genCls("list-item__icon-element")}>
+                  {item.avatar}
+                </span>
+              )
+            ) : (
+              ""
+            );
+            let extra = item.extra;
+            if (item.extra && item.status) {
+              const color = {
+                todo: "",
+                processing: "blue",
+                urgent: "red",
+                doing: "gold"
+              }[item.status];
+              extra = item.extra ? (
+                typeof item.extra === "string" ? (
+                  <ATag color={color} style={{ marginRight: 0 }}>
+                    {item.extra}
+                  </ATag>
+                ) : (
+                  item.extra
+                )
+              ) : (
+                ""
+              );
+            }
+            return (
+              <AListItem
+                class={itemCls}
+                key={item.key || i}
+                onClick={() => onClick && onClick(item)}
+              >
+                <AListItemMeta
+                  class={genCls("list-item__meta")}
+                  avatar={leftIcon}
+                  title={
+                    <div class={genCls("list-item__title")}>
+                      {item.title}
+                      <div class={genCls("list-item__extra")}>{extra}</div>
+                    </div>
+                  }
+                  description={
+                    <div>
+                      <div class={genCls("list-item__description")}>
+                        {item.description}
+                      </div>
+                      <div class={genCls("list-item__datetime")}>
+                        {item.datetime}
+                      </div>
+                    </div>
+                  }
+                />
+              </AListItem>
+            );
+          }}
+        />
+        <div class={genCls("bottom-bar")}>
+          {showClear ? (
+            <div onClick={onClear}>
+              {clearText} {title}
+            </div>
+          ) : (
+            ""
+          )}
+          {showViewMore ? (
+            <div onClick={e => onViewMore && onViewMore(e)}>{viewMoreText}</div>
+          ) : (
+            ""
+          )}
+        </div>
+      </div>
+    );
+  }
+};
+</script>
+
+<style lang="less" src="./NoticeList.less"></style>

+ 6 - 0
src/components/NoticeIcon/index.js

@@ -0,0 +1,6 @@
+import NoticeIcon from "./NoticeIcon.vue";
+import NoticeList from "./NoticeList.vue";
+NoticeIcon.Tab = NoticeList;
+
+export default NoticeIcon;
+export { NoticeList };

+ 3 - 0
src/layouts/Header.vue

@@ -1,5 +1,6 @@
 <template>
   <div class="header">
+    <notice-icon-view />
     <a-dropdown>
       <a-icon type="global" />
       <a-menu
@@ -19,7 +20,9 @@
 </template>
 
 <script>
+import NoticeIconView from "@/components/GlobalHeader/NoticeIconView";
 export default {
+  components: { NoticeIconView },
   methods: {
     localeChange({ key }) {
       this.$router.push({ query: { ...this.$route.query, locale: key } });

+ 12 - 1
src/locale/enUS.js

@@ -1,3 +1,14 @@
 export default {
-  "app.dashboard.analysis.timeLabel": "Time"
+  "app.dashboard.analysis.timeLabel": "Time",
+  "component.globalHeader.notification": "Notification",
+  "component.globalHeader.notification.empty":
+    "You have viewed all notifications.",
+  "component.globalHeader.message": "Message",
+  "component.globalHeader.message.empty": "You have viewed all messages.",
+  "component.globalHeader.event": "Event",
+  "component.globalHeader.event.empty": "You have viewed all events",
+  "component.noticeIcon.clear": "Clear",
+  "component.noticeIcon.cleared": "Cleared",
+  "component.noticeIcon.empty": "No notifications",
+  "component.noticeIcon.view-more": "View more"
 };

+ 11 - 1
src/locale/zhCN.js

@@ -1,3 +1,13 @@
 export default {
-  "app.dashboard.analysis.timeLabel": "时间"
+  "app.dashboard.analysis.timeLabel": "时间",
+  "component.globalHeader.notification": "通知",
+  "component.globalHeader.notification.empty": "您已查看所有通知",
+  "component.globalHeader.message": "消息",
+  "component.globalHeader.message.empty": "您已读完所有消息",
+  "component.globalHeader.event": "待办",
+  "component.globalHeader.event.empty": "您已完成所有待办",
+  "component.noticeIcon.clear": "清空",
+  "component.noticeIcon.cleared": "清空了",
+  "component.noticeIcon.empty": "暂无数据",
+  "component.noticeIcon.view-more": "查看更多"
 };

+ 15 - 1
src/main.js

@@ -20,7 +20,14 @@ import {
   Select,
   LocaleProvider,
   Dropdown,
-  DatePicker
+  DatePicker,
+  Badge,
+  Tabs,
+  Spin,
+  List,
+  Avatar,
+  Tag,
+  message
 } from "ant-design-vue";
 import Authorized from "./components/Authorized";
 import Auth from "./directives/auth";
@@ -44,6 +51,13 @@ Vue.component("Authorized", Authorized);
 Vue.use(Auth);
 Vue.use(VueI18n);
 Vue.use(VueHighlightJS);
+Vue.use(Badge);
+Vue.use(Tabs);
+Vue.use(Spin);
+Vue.use(List);
+Vue.use(Avatar);
+Vue.use(Tag);
+Vue.prototype.$message = message;
 
 const i18n = new VueI18n({
   locale: queryString.parse(location.search).locale || "zhCN",

+ 3 - 1
src/store/index.js

@@ -1,12 +1,14 @@
 import Vue from "vue";
 import Vuex from "vuex";
 import form from "./modules/form";
+import global from "./modules/global";
 
 Vue.use(Vuex);
 
 export default new Vuex.Store({
   state: {},
   modules: {
-    form
+    form,
+    global
   }
 });

+ 71 - 0
src/store/modules/global.js

@@ -0,0 +1,71 @@
+import request from "../../utils/request";
+
+const state = {
+  notices: [],
+  fetchingStatus: {
+    notice: false
+  },
+  notifyCount: 0,
+  unreadCount: 0
+};
+
+const actions = {
+  async fetchNotices({ commit, state }) {
+    commit("changeFetchStatus", { payload: { notice: true } });
+    const res = await request({
+      url: "/api/notices",
+      method: "GET"
+    });
+    const { data = [] } = res;
+    commit("saveNotices", { payload: data });
+    const unreadCount = state.notices.filter(item => !item.read).length;
+    commit("changeNotifyCount", { unreadCount, notifyCount: data.length });
+    commit("changeFetchStatus", { payload: { notice: false } });
+  },
+  clearNotices({ commit, state }, payload) {
+    commit("saveClearedNotices", { payload });
+    const count = state.notices.length;
+    const unreadCount = state.notices.filter(item => !item.read).length;
+    commit("changeNotifyCount", { unreadCount, notifyCount: count });
+  },
+  changeNoticeReadState({ commit, state }, payload) {
+    const notices = state.notices.map(item => {
+      const notice = { ...item };
+      if (notice.id === payload) {
+        notice.read = true;
+      }
+      return notice;
+    });
+    commit("saveNotices", { payload: notices });
+    commit("changeNotifyCount", {
+      notifyCount: notices.length,
+      unreadCount: notices.filter(item => !item.read).length
+    });
+  }
+};
+
+const mutations = {
+  changeFetchStatus(state, { payload }) {
+    state.fetchingStatus = {
+      ...state.fetchingStatus,
+      ...payload
+    };
+  },
+  saveClearedNotices(state, { payload }) {
+    state.notices = state.notices.filter(item => item.type !== payload);
+  },
+  saveNotices(state, { payload }) {
+    state.notices = payload;
+  },
+  changeNotifyCount(state, { notifyCount, unreadCount }) {
+    state.notifyCount = notifyCount;
+    state.unreadCount = unreadCount;
+  }
+};
+
+export default {
+  namespaced: true,
+  state,
+  actions,
+  mutations
+};

Một số tệp đã không được hiển thị bởi vì quá nhiều tập tin thay đổi trong này khác