Procházet zdrojové kódy

feat: 实现未读消息铃铛标志提示功能

zhouyuhao před 8 měsíci
rodič
revize
8b111fab1e

+ 9 - 0
src/api/module/notificationMessage.ts

@@ -77,3 +77,12 @@ export const hasUnreadMessages = () => {
     operate: 'check_notifications_message'
   })
 }
+
+/**
+ * 获取feature message详情数据
+ */
+export const getFeatureMsgPdf = () => {
+  return HttpAxios.get(`${baseUrl}`, {
+    action: 'feature_update'
+  })
+}

+ 66 - 25
src/components/NotificationMessageCard/src/NotificationMessageCard.vue

@@ -3,19 +3,24 @@ import EventCard from './components/EventCard.vue'
 import PasswordCard from './components/PasswordCard.vue'
 import FeatureUpdateCard from './components/FeatureUpdateCard.vue'
 import { useNotificationMessage } from '@/stores/modules/notificationMessage'
+import { cloneDeep } from 'lodash'
 
 const notificationMsgStore = useNotificationMessage()
 const props = withDefaults(
   defineProps<{
     data: any
-    isObserver?: boolean
+    isObserver?: boolean // 是否开启监听
+    updateReadCardsOnChange?: boolean // 是否在数据变化时请求接口更新已读卡片
   }>(),
   {
-    isObserver: true
+    isObserver: true,
+    updateReadCardsOnChange: true
   }
 )
 
-const emit = defineEmits<{ seeAll: [] }>()
+const pageData = ref<any[]>([])
+
+const emit = defineEmits<{ seeAll: []; hasCardRead: [] }>()
 const handleSeeAll = () => {
   emit('seeAll')
 }
@@ -24,17 +29,17 @@ const handleSeeAll = () => {
 const observer = new IntersectionObserver(
   (entries) => {
     entries.forEach((entry) => {
-      // const index = cardsTest.value.findIndex((item) => item.id === entry.target?.dataset?.cardId)
       const cardId = entry.target?.dataset?.cardId
       if (entry.isIntersecting) {
         // 将卡片设置为已经展示
         notificationMsgStore.setReadCardMap(cardId)
-        const index = props.data.findIndex((item) => item.info.id === cardId)
+        const index = pageData.value.findIndex((item) => item.info.id === cardId)
         if (index > -1) {
           //  在1秒钟后将消息卡片设置为已读
           setTimeout(() => {
-            props.data[index].info.isRead = true
-          }, 500)
+            pageData.value[index].info.isRead = true
+            emit('hasCardRead')
+          }, 400)
         }
       }
     })
@@ -45,7 +50,7 @@ const observer = new IntersectionObserver(
     threshold: 0.1 // 当至少10%的元素进入视图时触发回调
   }
 )
-const curPageAllCards = ref()
+const curPageAllCards = ref<any>([])
 // 监听元素是否在可视区域内
 const watchCards = () => {
   curPageAllCards.value = document?.querySelectorAll('.notification-message-card')
@@ -60,14 +65,60 @@ const watchCards = () => {
   })
 }
 
+const clearReadData = (data) => {
+  const curData = data || []
+  // 将当前页面剩余未读消息id从监听列表中移除
+  const idsToRemove = curData.map((item: any) => item.info.id)
+  notificationMsgStore.removeNotificationMsgList(idsToRemove)
+
+  // 清除当前页面的所有卡片的监听
+  curPageAllCards.value.forEach((card: any) => {
+    observer.unobserve(card)
+  })
+}
+
+const compareIdsInArrays = (arr1, arr2) => {
+  // 如果两个数组长度不同,则它们不可能有相同的id集合
+  if (arr1.length !== arr2.length) {
+    return false
+  }
+
+  // 提取两个数组中的所有有效id并转换为Set
+  const ids1 = new Set(arr1.filter((item) => item.info && item.info.id).map((item) => item.info.id))
+  const ids2 = new Set(arr2.filter((item) => item.info && item.info.id).map((item) => item.info.id))
+
+  // 比较两个Set的大小是否相同
+  if (ids1.size !== ids2.size) {
+    return false
+  }
+
+  // 检查ids1中的每个id是否都存在于ids2中
+  for (let id of ids1) {
+    if (!ids2.has(id)) {
+      return false
+    }
+  }
+
+  return true
+}
+
 if (props.isObserver) {
   watch(
     () => props.data,
-    () => {
+    (newData, oldData) => {
+      // 因为当父组件中数据已读未读状态变化时,也会触发props.data变化,此时不需要更新页面数据,根据id是否相等来判断这种情况
+      if (compareIdsInArrays(oldData || [], newData || [])) return
+
+      pageData.value = cloneDeep(newData)
+      // 先清除旧数据中的卡片监听
+      clearReadData(oldData)
+
+      // 请求接口将旧数据中的新已读卡片上传服务器
+      props.data.updateReadCardsOnChange && notificationMsgStore.markMessageAsRead()
+
+      // 重新监听新数据中的卡片
       nextTick(() => {
-        setTimeout(() => {
-          watchCards()
-        }, 500)
+        watchCards()
       })
     },
     {
@@ -77,17 +128,7 @@ if (props.isObserver) {
   )
 }
 
-const clearReadData = () => {
-  const idsToRemove = props.data.map((item: any) => item.info.id)
-  notificationMsgStore.removeNotificationMsgList(idsToRemove)
-
-  // 清除当前页面的所有卡片的监听
-  curPageAllCards.value.forEach((card: any) => {
-    observer.unobserve(card)
-  })
-}
-
-// 定时将消息卡片未读的置为已读
+// 定时将消息卡片未读的置为已读,五分钟一次
 let timer = null
 onMounted(() => {
   if (props.isObserver) {
@@ -99,7 +140,7 @@ onMounted(() => {
 onUnmounted(() => {
   if (props.isObserver) {
     notificationMsgStore.markMessageAsRead()
-    clearReadData()
+    clearReadData(pageData.value)
     clearInterval(timer)
   }
 })
@@ -110,7 +151,7 @@ onUnmounted(() => {
     class="notification-message-card"
     :data-card-id="item.info.id"
     :data-card-isread="item.info.isRead"
-    v-for="item in data"
+    v-for="item in pageData"
     :key="item.info.id || Math.random()"
   >
     <EventCard @seeAll="handleSeeAll" v-if="item.notificationType === 'event'" :data="item.info" />

+ 3 - 1
src/components/NotificationMessageCard/src/components/FeatureUpdateCard.vue

@@ -4,6 +4,7 @@ import { useRouter } from 'vue-router'
 const router = useRouter()
 
 interface FeatureUpdateCardPropsData {
+  id: string
   title: string
   header: string
   content: string
@@ -20,7 +21,8 @@ const handleViewMore = () => {
     name: 'System Message Detail',
     query: {
       type: 'feature',
-      title: props.data.header
+      title: props.data.header,
+      id: props.data.id
     }
   })
 }

+ 9 - 0
src/router/index.ts

@@ -133,6 +133,15 @@ router.beforeEach(async (to, from, next) => {
     sessionStorage.removeItem('bookingTablePageInfo')
   }
 
+  // 判断是否从systemMessage详情页跳转到systemMessage列表页,或者从systemMessage列表页跳转到systemMessage详情页
+  if (
+    !(from.name === 'System Message Detail' || from.name === 'System Message' || !from.name) ||
+    !(to.name === 'System Message' || to.name === 'System Message Detail')
+  ) {
+    console.log(from, 'router', to)
+    sessionStorage.removeItem('activeCardTypeName')
+  }
+
   // 未登录白名单
   const whiteList = ['/login', '/public-tracking', '/public-tracking/detail', '/reset-password']
   // 判断是否登录

+ 4 - 0
src/stores/modules/notificationMessage.ts

@@ -42,6 +42,10 @@ export const useNotificationMessage = defineStore('notificationMessage', {
         }
       })
     },
+    setHasNewMsg() {
+      this.hasNewMsg = true
+      localStorage.setItem('hasNewMsg', JSON.stringify(this.hasNewMsg))
+    },
     async markMessageAsRead() {
       if (this.readCardMap.length === 0) return
       console.log('置为已读')

+ 1 - 1
src/views/Booking/src/components/BookingDetail/src/components/EmailView.vue

@@ -106,7 +106,7 @@ onMounted(async () => {
     replaceSvgByDataKey(item.dataMenuKey, svgUrl)
   }
 })
-function replaceSvgByDataKey(dataMenuKey: any, svgUrl: any) {
+const replaceSvgByDataKey = (dataMenuKey: any, svgUrl: any) => {
   const observer = new MutationObserver((mutationsList, observer) => {
     const element = document.querySelector(`[data-menu-key="${dataMenuKey}"]`)
     if (element) {

+ 0 - 16
src/views/Layout/src/components/Header/components/NotificationDrawer.vue

@@ -1,9 +1,7 @@
 <script setup lang="ts">
 import NotificationMessageCard from '@/components/NotificationMessageCard/src/NotificationMessageCard.vue'
 import { useRouter } from 'vue-router'
-import { useNotificationMessage } from '@/stores/modules/notificationMessage'
 
-const notificationMsgStore = useNotificationMessage()
 const router = useRouter()
 const loading = ref(false)
 
@@ -65,25 +63,11 @@ const handleSettingMessage = () => {
 }
 
 const closeDrawer = () => {
-  const idsToRemove = notificationList.value.map((item) => item.info.id)
-  notificationMsgStore.removeNotificationMsgList(idsToRemove)
-
-  notificationMsgStore.markMessageAsRead()
-  clearInterval(timer)
-
   notificationList.value = []
   notificationType.value = 'all'
 }
 
 const notificationListRef = ref<HTMLElement | null>(null)
-
-// 定时将消息卡片未读的置为已读
-let timer = null
-onMounted(() => {
-  timer = setInterval(() => {
-    notificationMsgStore.markMessageAsRead()
-  }, 300000)
-})
 </script>
 
 <template>

+ 8 - 2
src/views/Layout/src/components/Header/components/TrainingCard.vue

@@ -2,9 +2,12 @@
 import { onBeforeRouteUpdate } from 'vue-router'
 import NotificationMessageCard from '@/components/NotificationMessageCard/src/NotificationMessageCard.vue'
 import { useUserStore } from '@/stores/modules/user'
+import { useNotificationMessage } from '@/stores/modules/notificationMessage'
 import dayjs from 'dayjs'
 
 const userStore = useUserStore()
+const notificationMsgStore = useNotificationMessage()
+
 const notificationList = ref([])
 
 const curCard = computed(() => {
@@ -82,10 +85,13 @@ const getNotificationList = (time?: string) => {
     })
     .then((res) => {
       if (res.code === 200) {
+        const data = res.data
         curIndex.value = 0
-        notificationList.value = res.data
+        notificationList.value = data
+
+        data.length > 0 && notificationMsgStore.setHasNewMsg()
 
-        if (time && res.data.length > 0) {
+        if (time && data.length > 0) {
           initTrainingCard()
         } else {
           trainingIntervalId = setInterval(trainingCardAfterLogin, 2000)

+ 71 - 34
src/views/SystemMessage/src/SystemMessage.vue

@@ -2,14 +2,30 @@
 import NotificationMessageCard from '@/components/NotificationMessageCard/src/NotificationMessageCard.vue'
 import { useNotificationMessage } from '@/stores/modules/notificationMessage'
 
+const activeCardTypeName = ref(sessionStorage.getItem('activeCardTypeName') || 'Milestone Update')
+
 const notificationMsgStore = useNotificationMessage()
 const collapseVModel = ref<string[]>(['1'])
 
 const tabCountList = ref([0, 0, 0, 0, 0])
+const curTabCount = ref([])
+
 const handleCount = (count: number) => {
-  if (!count) return 0
+  if (!count) return ''
   return count > 99 ? '99+' : count
 }
+const handleShowCount = (typeName: string, index: number) => {
+  // 在切换type类型时,防止点击的类型值为点击之前类型的值
+  if (curTabCount.value?.[index] > -1) {
+    return curTabCount.value[index]
+  }
+  const count = tabCountList.value[index]
+  if (typeName === activeCardTypeName.value) {
+    return handleCount(unreadNotificationList.value.length)
+  }
+  return handleCount(count)
+}
+
 const navList = [
   'Milestone Update',
   'Container Status Update',
@@ -17,7 +33,6 @@ const navList = [
   'ETD/ETA Change'
 ]
 
-const activeItem = ref('Milestone Update')
 const notificationTypeList = ref({
   Milestone_Update: 'Milestone Update',
   Container_Status_Update: 'Container Status Update',
@@ -28,17 +43,23 @@ const notificationTypeList = ref({
 })
 
 const setActiveItem = (item: string) => {
-  activeItem.value = item
-  activeName.value = 'All Notifications'
-
-  const idsToRemove = notificationList.value.map((item) => item.info.id)
-  notificationMsgStore.removeNotificationMsgList(idsToRemove)
+  navList.forEach((navItem, index) => {
+    curTabCount.value[index] = handleShowCount(navItem, index)
+  })
+  curTabCount.value[tabCountList.value.length - 1] = handleShowCount(
+    'Feature Update',
+    tabCountList.value.length - 1
+  )
 
+  activeCardTypeName.value = item
+  sessionStorage.setItem('activeCardTypeName', item)
+  activeTabName.value = 'All Notifications'
   getNotificationList()
 }
 
 const loading = ref(false)
 const notificationList = ref<any[]>([])
+
 const unreadNotificationList = computed(() => {
   return notificationList.value.filter((item) => !item.info.isRead)
 })
@@ -48,7 +69,7 @@ const readNotificationList = computed(() => {
 const getNotificationList = async () => {
   loading.value = true
   const rulesType = Object.entries(notificationTypeList.value).find(
-    (item) => item[1] === activeItem.value
+    (item) => item[1] === activeCardTypeName.value
   )?.[0]
   try {
     await notificationMsgStore.markMessageAsRead()
@@ -65,25 +86,38 @@ const getNotificationList = async () => {
       })
       .finally(() => {
         loading.value = false
+        curTabCount.value = []
       })
   } catch (error) {
     console.error(error)
     loading.value = false
+    curTabCount.value = []
   }
 }
-// 获取未读消息数量
-const getUnReadCount = () => {
-  return notificationList.value.filter((item) => !item.info.isRead).length
+
+const changeCardRead = () => {
+  const readCardMap = notificationMsgStore.readCardMap
+  notificationList.value.forEach((item) => {
+    if (readCardMap.includes(item.info.id)) {
+      item.info.isRead = true
+    }
+  })
 }
 
-const activeName = ref('All Notifications')
+const activeTabName = ref('All Notifications')
+const handleTabChange = () => {
+  // 当前tab页切换时,更新数据
+  const readCardMap = notificationMsgStore.readCardMap
+  notificationList.value.forEach((item) => {
+    if (readCardMap.includes(item.info.id)) {
+      item.info.isRead = true
+    }
+  })
+}
 
 onMounted(() => {
   getNotificationList()
 })
-onUnmounted(() => {
-  notificationMsgStore.markMessageAsRead()
-})
 </script>
 
 <template>
@@ -96,18 +130,14 @@ onUnmounted(() => {
             <div
               @click="setActiveItem(item)"
               class="collapse-item"
-              :class="{ 'is-active': item === activeItem }"
+              :class="{ 'is-active': item === activeCardTypeName }"
               v-for="(item, index) in navList"
               :key="item"
             >
-              <div v-if="item === activeItem" class="active-sign"></div>
+              <div v-if="item === activeCardTypeName" class="active-sign"></div>
               <span>{{ item }}</span>
-              <div class="count" v-if="tabCountList?.[index]">
-                <span>{{
-                  item === activeItem
-                    ? handleCount(getUnReadCount())
-                    : handleCount(tabCountList?.[index])
-                }}</span>
+              <div class="count" v-if="handleShowCount(item, index)">
+                <span>{{ handleShowCount(item, index) }}</span>
               </div>
             </div>
           </el-collapse-item>
@@ -116,25 +146,27 @@ onUnmounted(() => {
           @click="setActiveItem('Feature Update')"
           class="collapse-item"
           style="margin-top: 4px; font-weight: 700"
-          :class="{ 'is-active': activeItem === 'Feature Update' }"
+          :class="{ 'is-active': activeCardTypeName === 'Feature Update' }"
         >
-          <div v-if="activeItem === 'Feature Update'" class="active-sign"></div>
+          <div v-if="activeCardTypeName === 'Feature Update'" class="active-sign"></div>
           <span>Feature Update</span>
-          <div class="count" v-if="tabCountList?.[tabCountList.length - 1]">
-            <span>{{ handleCount(tabCountList?.[tabCountList.length - 1]) }}</span>
+          <div class="count" v-if="handleShowCount('Feature Update', tabCountList.length - 1)">
+            <span>{{ handleShowCount('Feature Update', tabCountList.length - 1) }}</span>
           </div>
         </div>
       </div>
       <div class="right-content">
-        <el-tabs v-model="activeName" class="demo-tabs">
+        <el-tabs v-model="activeTabName" @tab-change="handleTabChange" class="demo-tabs">
           <el-tab-pane label="All Notifications" name="All Notifications">
             <template #label>
               <span style="margin-right: 4px">All Notifications</span>
             </template>
-            <div style="padding: 10px 140px 0px 16px" v-if="activeName === 'All Notifications'">
+            <div style="padding: 10px 140px 0px 16px" v-if="activeTabName === 'All Notifications'">
               <NotificationMessageCard
-                v-if="activeName === 'All Notifications'"
+                v-if="activeTabName === 'All Notifications'"
                 :data="notificationList"
+                @hasCardRead="changeCardRead"
+                :updateReadCardsOnChange="false"
               ></NotificationMessageCard>
             </div>
           </el-tab-pane>
@@ -145,17 +177,22 @@ onUnmounted(() => {
                 <span>{{ handleCount(unreadNotificationList.length) }}</span>
               </div>
             </template>
-            <div style="padding: 10px 140px 0px 16px" v-if="activeName === 'Unread'">
+            <div style="padding: 10px 140px 0px 16px" v-if="activeTabName === 'Unread'">
               <NotificationMessageCard
-                v-if="activeName === 'Unread'"
+                v-if="activeTabName === 'Unread'"
                 :data="unreadNotificationList"
+                :updateReadCardsOnChange="false"
               ></NotificationMessageCard>
             </div>
           </el-tab-pane>
           <el-tab-pane label="Read" name="Read">
             <template #label><span style="margin-right: 4px">Read</span> </template>
-            <div style="padding: 10px 140px 0px 16px" v-if="activeName === 'Read'">
-              <NotificationMessageCard v-if="activeName === 'Read'" :data="readNotificationList">
+            <div style="padding: 10px 140px 0px 16px" v-if="activeTabName === 'Read'">
+              <NotificationMessageCard
+                v-if="activeTabName === 'Read'"
+                :updateReadCardsOnChange="false"
+                :data="readNotificationList"
+              >
               </NotificationMessageCard>
             </div>
           </el-tab-pane>

+ 16 - 6
src/views/SystemMessage/src/components/SystemMessageDetail.vue

@@ -32,7 +32,6 @@ const getNotificationList = () => {
 }
 const init = () => {
   if (route.query.type === 'feature') {
-    console.log(route.query.title)
     notificationData.value.title = (route.query.title as string) || ''
   } else {
     getNotificationList()
@@ -41,10 +40,20 @@ const init = () => {
 onMounted(() => {
   init()
 })
+
+const iframeRef = ref(null)
+const errorAA = (e) => {
+  nextTick(() => {
+    console.dir(iframeRef.value, 'iframeRef')
+  })
+}
+
+const iframeUrl = `${import.meta.env.VITE_API_HOST}/main_new_version.php?action=feature_update&id=${route.query.id}`
 </script>
 
 <template>
   <div class="system-message-detail" v-vloading="loading">
+    <!-- <el-button @click="errorAA">测试</el-button> -->
     <div class="content" v-if="route.query.type !== 'feature'">
       <div class="header" v-if="notificationData.title">
         <div class="status-icon"></div>
@@ -85,12 +94,13 @@ onMounted(() => {
         <div class="title">{{ notificationData.title }}</div>
       </div>
       <div class="feature-pdf">
-        <embed
-          src="https://juejin.cn/post/7480076971226284058?utm_source=gold_browser_extension"
+        <iframe
+          ref="iframeRef"
+          :src="iframeUrl"
           width="100%"
           height="100%"
-          type="application/pdf"
-        />
+          @load="errorAA"
+        ></iframe>
       </div>
     </div>
   </div>
@@ -103,7 +113,7 @@ onMounted(() => {
   overflow: auto;
   .content {
     margin: auto;
-    width: 800px;
+    width: 1000px;
     height: 100%;
   }
   .notification-card {

+ 1 - 1
src/views/Tracking/src/components/TrackingDetail/src/components/EmailDrawer.vue

@@ -103,7 +103,7 @@ onMounted(async () => {
     replaceSvgByDataKey(item.dataMenuKey, svgUrl)
   }
 })
-function replaceSvgByDataKey(dataMenuKey: any, svgUrl: any) {
+const replaceSvgByDataKey = (dataMenuKey: any, svgUrl: any) => {
   const observer = new MutationObserver((mutationsList, observer) => {
     const element = document.querySelector(`[data-menu-key="${dataMenuKey}"]`)
     if (element) {

+ 1 - 1
src/views/Tracking/src/components/TrackingDetail/src/components/UploadFilesDialog.vue

@@ -34,7 +34,7 @@ const changeFileList = (file: any, fileList: any) => {
   }
 }
 
-function bytesToKB(bytes: number) {
+const bytesToKB = (bytes: number) => {
   return (bytes / 1024).toFixed(0) // 将字节转换为KB,保留两位小数
 }