瀏覽代碼

Merge branch 'dev_zyh' of United_Software/k_online_ui into feature

Jack Zhou 8 月之前
父節點
當前提交
1669e075d5
共有 29 個文件被更改,包括 954 次插入838 次删除
  1. 47 2
      src/api/module/notificationMessage.ts
  2. 2 1
      src/auto-imports.d.ts
  3. 1 1
      src/components/DateRange/src/components/CalendarDate.vue
  4. 165 9
      src/components/NotificationMessageCard/src/NotificationMessageCard.vue
  5. 66 51
      src/components/NotificationMessageCard/src/components/EventCard.vue
  6. 46 3
      src/components/NotificationMessageCard/src/components/FeatureUpdateCard.vue
  7. 24 6
      src/components/NotificationMessageCard/src/components/PasswordCard.vue
  8. 二進制
      src/components/NotificationMessageCard/src/images/test.png
  9. 21 1
      src/components/VSliderVerification/src/VSliderVerification.vue
  10. 71 0
      src/stores/modules/notificationMessage.ts
  11. 2 0
      src/stores/modules/user.ts
  12. 1 1
      src/styles/elementui.scss
  13. 30 0
      src/styles/theme.scss
  14. 1 1
      src/views/Booking/src/components/BookingDetail/src/components/EmailView.vue
  15. 2 2
      src/views/Dashboard/src/components/RecentStatus.vue
  16. 16 4
      src/views/Layout/src/components/Header/HeaderView.vue
  17. 47 80
      src/views/Layout/src/components/Header/components/NotificationDrawer.vue
  18. 90 125
      src/views/Layout/src/components/Header/components/TrainingCard.vue
  19. 0 43
      src/views/Layout/src/components/Menu/MenuView.vue
  20. 2 1
      src/views/Login/src/loginView.vue
  21. 180 220
      src/views/SystemMessage/src/SystemMessage.vue
  22. 111 45
      src/views/SystemMessage/src/components/SystemMessageDetail.vue
  23. 12 12
      src/views/SystemSettings/src/SystemSettings.vue
  24. 2 1
      src/views/Tracking/src/components/PublicTracking/src/PublicTrackingSearch.vue
  25. 0 222
      src/views/Tracking/src/components/PublicTracking/src/components/SlideVerify.vue
  26. 11 3
      src/views/Tracking/src/components/TrackingDetail/src/TrackingDetail.vue
  27. 1 1
      src/views/Tracking/src/components/TrackingDetail/src/components/EmailDrawer.vue
  28. 1 1
      src/views/Tracking/src/components/TrackingDetail/src/components/UploadFilesDialog.vue
  29. 2 2
      src/views/Tracking/src/components/TrackingTable/src/TrackingTable.vue

+ 47 - 2
src/api/module/notificationMessage.ts

@@ -20,8 +20,9 @@ export const saveUserInfo = (params: any) => {
 }
 
 /**
- * 获取milestone消息列表
- * @param save_model profile 代表基本信息的save, no_profile 代表格式信息的save
+ * 获取notification消息列表
+ * @param rules_type 特定类型的消息,all查全部
+ * @param current_time 当前时间 轮询查询时使用
  */
 export const getNotificationList = (params: any) => {
   return HttpAxios.get(`${baseUrl}`, {
@@ -41,3 +42,47 @@ export const getNotificationDetails = (params: any) => {
     ...params
   })
 }
+
+/**
+ * 获取system message页面数据
+ */
+export const getSystemMessageData = (params: any) => {
+  return HttpAxios.get(`${baseUrl}`, {
+    action: 'notifications_rules',
+    operate: 'notifications_message_init',
+    ...params
+  })
+}
+
+/**
+ * 将notification消息标记为已读
+ * @param id 消息id Array
+ * @param read_type 如果标记全部为已读 = true. 否则其他情况为false
+ */
+export const setMessageRead = (params: any) => {
+  return HttpAxios.post(`${baseUrl}`, {
+    action: 'notifications_rules',
+    operate: 'notifications_read',
+    read_type: false,
+    ...params
+  })
+}
+
+/**
+ * 检测是否有新消息
+ */
+export const hasUnreadMessages = () => {
+  return HttpAxios.post(`${baseUrl}`, {
+    action: 'notifications_rules',
+    operate: 'check_notifications_message'
+  })
+}
+
+/**
+ * 获取feature message详情数据
+ */
+export const getFeatureMsgPdf = () => {
+  return HttpAxios.get(`${baseUrl}`, {
+    action: 'feature_update'
+  })
+}

+ 2 - 1
src/auto-imports.d.ts

@@ -3,6 +3,7 @@
 // @ts-nocheck
 // noinspection JSUnusedGlobalSymbols
 // Generated by unplugin-auto-import
+// biome-ignore lint: disable
 export {}
 declare global {
   const $api: typeof import('@/api/index')['default']
@@ -68,6 +69,6 @@ declare global {
 // for type re-export
 declare global {
   // @ts-ignore
-  export type { Component, ComponentPublicInstance, ComputedRef, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, VNode, WritableComputedRef } from 'vue'
+  export type { Component, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
   import('vue')
 }

+ 1 - 1
src/components/DateRange/src/components/CalendarDate.vue

@@ -23,7 +23,7 @@ const props = defineProps({
     default: false
   }
 })
-const ETDDate = ref()
+const ETDDate = ref([])
 watch(
   () => props.Date,
   (current: any) => {

+ 165 - 9
src/components/NotificationMessageCard/src/NotificationMessageCard.vue

@@ -2,19 +2,175 @@
 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 props = defineProps<{
-  data: any
-}>()
+const notificationMsgStore = useNotificationMessage()
+const props = withDefaults(
+  defineProps<{
+    data: any
+    isObserver?: boolean // 是否开启监听
+    updateReadCardsOnChange?: boolean // 是否在数据变化时请求接口更新已读卡片
+  }>(),
+  {
+    isObserver: true,
+    updateReadCardsOnChange: true
+  }
+)
+
+const pageData = ref<any[]>([])
+
+const emit = defineEmits<{ seeAll: []; hasCardRead: []; viewMore: []; jumpTracking: [] }>()
+const handleSeeAll = () => {
+  emit('seeAll')
+}
+
+// 创建一个新的 IntersectionObserver 实例
+const observer = new IntersectionObserver(
+  (entries) => {
+    entries.forEach((entry) => {
+      const cardId = entry.target?.dataset?.cardId
+      if (entry.isIntersecting) {
+        // 将卡片设置为已经展示
+        notificationMsgStore.setReadCardMap(cardId)
+        const index = pageData.value.findIndex((item) => item.info.id === cardId)
+        if (index > -1) {
+          //  在1秒钟后将消息卡片设置为已读
+          setTimeout(() => {
+            pageData.value[index].info.isRead = true
+            emit('hasCardRead')
+          }, 400)
+        }
+      }
+    })
+  },
+  {
+    // 配置选项
+    root: null, // 使用视窗作为根元素
+    threshold: 0.1 // 当至少10%的元素进入视图时触发回调
+  }
+)
+const curPageAllCards = ref<any>([])
+// 监听元素是否在可视区域内
+const watchCards = () => {
+  curPageAllCards.value = document?.querySelectorAll('.notification-message-card')
+  curPageAllCards.value.forEach((card: any) => {
+    // const index = notificationMsgStore.notificationMsgList.indexOf(card.dataset.cardId)
+    // index < 0 &&
+    if (card.dataset.cardIsread === 'false' && card.dataset.cardId) {
+      notificationMsgStore.concatNotificationMsgList([card.dataset.cardId])
+
+      observer.observe(card)
+    }
+  })
+}
+
+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(() => {
+        watchCards()
+      })
+    },
+    {
+      immediate: true,
+      deep: true
+    }
+  )
+}
+
+// 定时将消息卡片未读的置为已读,五分钟一次
+let timer = null
+onMounted(() => {
+  if (props.isObserver) {
+    timer = setInterval(() => {
+      notificationMsgStore.markMessageAsRead()
+    }, 300000)
+  }
+})
+onUnmounted(() => {
+  if (props.isObserver) {
+    notificationMsgStore.markMessageAsRead()
+    clearReadData(pageData.value)
+    clearInterval(timer)
+  }
+})
+
+const handleViewMore = () => {
+  emit('viewMore')
+}
 </script>
 
 <template>
-  <div class="notification-message-card">
-    <template v-for="(item, index) in data" :key="index">
-      <EventCard v-if="item.notificationType === 'event'" :data="item.info" />
-      <PasswordCard v-else-if="item.notificationType === 'password'" :data="item.info" />
-      <FeatureUpdateCard v-else-if="item.notificationType === 'feature'" :data="item.info" />
-    </template>
+  <div
+    class="notification-message-card"
+    :data-card-id="item.info.id"
+    :data-card-isread="item.info.isRead"
+    v-for="item in pageData"
+    :key="item.info.id || Math.random()"
+  >
+    <EventCard
+      @seeAll="handleSeeAll"
+      @jump-tracking="emit('jumpTracking')"
+      v-if="item.notificationType === 'event'"
+      :data="item.info"
+    />
+    <PasswordCard v-else-if="item.notificationType === 'password'" :data="item.info" />
+    <FeatureUpdateCard
+      @view-more="handleViewMore"
+      v-else-if="item.notificationType === 'feature'"
+      :data="item.info"
+    />
+    <slot></slot>
   </div>
 </template>
 

+ 66 - 51
src/components/NotificationMessageCard/src/components/EventCard.vue

@@ -18,7 +18,17 @@ interface EventCardPropsData {
   timezone?: string // 时区
   time: string
   timeLabel: string
-  previous?: Array<string>
+  serial_no?: string // 单号 用来跳转到Tracking详情页
+  order_from?: string // 订单来源 用来跳转到Tracking详情页
+  insert_date_format?: string // 用来跳转到System Message详情页
+  frequency_type?: string // 用来跳转到System Message详情页
+  rules_type?: string // 用来跳转到System Message详情页
+  previous?: {
+    date: string
+    tag: string
+    time: string
+    timezone: string
+  }
   info?: {
     route?: []
     etdOrdeparturNum?: number
@@ -35,14 +45,26 @@ const props = defineProps<{
   data: EventCardPropsData
 }>()
 
-const handleSeeAll = (data: any) => {
+const emit = defineEmits<{ seeAll: []; jumpTracking: [] }>()
+const handleSeeAll = (data: EventCardPropsData) => {
+  emit('seeAll')
   router.push({
     name: 'System Message Detail',
     query: {
-      frequency_type: data.frequency_type
+      frequency_type: data.frequency_type,
+      insert_date_format: data.insert_date_format,
+      rules_type: data.rules_type
     }
   })
 }
+
+const jumpTracking = (data: EventCardPropsData) => {
+  emit('jumpTracking')
+  router.push({
+    path: '/tracking/detail',
+    query: { a: data.serial_no, _schemas: data.order_from }
+  })
+}
 </script>
 
 <template>
@@ -54,48 +76,28 @@ const handleSeeAll = (data: any) => {
     <div class="content">
       <div
         class="more-tips"
-        v-if="(data.type === 'milestone' || 'container') && data.numericRecords"
+        v-if="(data.type === 'milestone' || data.type === 'container') && data.numericRecords"
       >
-        <span>Latest Status Updates ({{ data.numericRecords }})</span>
+        <span>Latest Status Updates ({{ data.numericRecords }}) </span>
         <el-button @click="handleSeeAll(data)" class="see-all-icon el-button--text">
           See All
           <span class="font_family icon-icon_next_b"></span>
         </el-button>
       </div>
-      <div
-        class="more-tips"
-        v-if="data.type === 'delay' && (data.info.etdOrdeparturNum || data.info.etaOrarrivalNum)"
-      >
+      <div class="more-tips" v-if="data.info?.etdOrdeparturNum || data.info?.etaOrarrivalNum">
         <div>
-          <span v-if="data.info.etdOrdeparturNum"
-            >Departure Delay ({{ data.info.etdOrdeparturNum }})</span
+          <span v-if="data.info?.etdOrdeparturNum"
+            >{{ data.type === 'delay' ? 'Departure Delay' : 'ETD Change' }} ({{
+              data.info?.etdOrdeparturNum
+            }})</span
           >
-          <span v-if="data.info.etdOrdeparturNum && data.info.etaOrarrivalNum">
+          <span v-if="data.info?.etdOrdeparturNum && data.info?.etaOrarrivalNum">
             &nbsp;&nbsp;|&nbsp;&nbsp;</span
           >
-          <span v-if="data.info.etaOrarrivalNum">
-            Arrival Delay ({{ data.info.etaOrarrivalNum }})
-          </span>
-        </div>
-        <el-button @click="handleSeeAll(data)" class="see-all-icon el-button--text">
-          See All
-          <span class="font_family icon-icon_next_b"></span>
-        </el-button>
-      </div>
-
-      <div
-        class="more-tips"
-        v-if="data.type === 'change' && (data.info.etdOrdeparturNum || data.info.etaOrarrivalNum)"
-      >
-        <div>
-          <span v-if="data.info.etdOrdeparturNum"
-            >ETD Change ({{ data.info.etdOrdeparturNum }})</span
-          >
-          <span v-if="data.info.etdOrdeparturNum && data.info.etaOrarrivalNum">
-            &nbsp;&nbsp;|&nbsp;&nbsp;</span
-          >
-          <span v-if="data.info.etaOrarrivalNum">
-            ETA Change ({{ data.info.etaOrarrivalNum }})
+          <span v-if="data.info?.etaOrarrivalNum">
+            {{ data.type === 'delay' ? 'Arrival Delay' : 'ETA Change' }} ({{
+              data.info?.etaOrarrivalNum
+            }})
           </span>
         </div>
         <el-button @click="handleSeeAll(data)" class="see-all-icon el-button--text">
@@ -108,7 +110,7 @@ const handleSeeAll = (data: any) => {
         <!-- 除了container类型,其他类型都显示运输方式图标 -->
         <div style="display: inline-block" v-if="data.type !== 'container'">
           <span class="font_family" :class="[`icon-${transportationMode?.[data.mode]}`]"></span>
-          <span class="no">HBOL: {{ data.no }}</span>
+          <span @click="jumpTracking(data)" class="no no-link">HBOL: {{ data.no }}</span>
         </div>
         <!-- container类型显示图标 -->
         <div v-else>
@@ -143,15 +145,15 @@ const handleSeeAll = (data: any) => {
       </div>
       <div
         :class="{ 'delay-time': data.type === 'delay', 'change-time': data.type === 'change' }"
-        v-if="(data.type === 'delay' || 'change') && data.info?.time"
+        v-if="(data.type === 'delay' || data.type === 'change') && data.info?.time"
       >
         <span
           v-if="data.type === 'delay'"
-          style="margin-right: 5px"
+          style="margin-right: 6px"
           class="font_family icon-icon_delay_b"
         ></span>
         <span v-else class="font_family icon-icon_time_b"></span>
-        <span style="margin-right: 2px">{{ data.info.timeLabel }}&nbsp;</span>
+        <span style="margin-right: 2px" v-if="data.info.timeLabel">{{ data.info.timeLabel }}:</span>
         <span style="margin-right: 3px">{{
           dayjs(data.info.time).format('MMM DD, YYYY hh:mm')
         }}</span>
@@ -163,13 +165,16 @@ const handleSeeAll = (data: any) => {
       </div> -->
       <div class="time" :class="{ grey: data.type === 'delay' || data.type === 'change' }">
         <span class="font_family icon-icon_time_b"></span>
-        <span style="margin-right: 2px">{{ data.timeLabel }}</span>
+        <span style="margin-right: 3px" v-if="data.timeLabel">{{ data.timeLabel }}:</span>
         <span style="margin-right: 3px">{{ dayjs(data.time).format('MMM DD, YYYY hh:mm') }}</span>
         <span>{{ getTimezone(data.timezone) }}</span>
       </div>
       <div class="previous" v-if="data.previous">
         <span class="previous-icon"></span>
-        <span>{{ data.previous }}</span>
+        <span
+          >{{ data.previous.tag }}&nbsp;({{ data.previous.time }}
+          {{ getTimezone(data.previous.timezone) }})</span
+        >
       </div>
     </div>
   </div>
@@ -201,6 +206,7 @@ const handleSeeAll = (data: any) => {
       margin-right: 10px;
     }
     .title {
+      flex: 1;
       font-weight: 700;
     }
   }
@@ -232,16 +238,24 @@ const handleSeeAll = (data: any) => {
         font-weight: 700;
         line-height: 18px;
       }
+      .no-link {
+        &:hover {
+          color: var(--color-theme);
+          cursor: pointer;
+        }
+      }
       .tag {
         display: flex;
         align-items: center;
+        height: 18px;
+        margin-top: 2px;
         margin-left: 4px;
         padding-left: 8px;
         padding-right: 6px;
-        background-color: #e6f1eb;
+        background-color: var(--color-milestone-tag-bg);
         border-radius: 3px;
         &.delay {
-          background-color: #f7e7e9;
+          background-color: var(--color-delay-tag-bg);
           .dot {
             background-color: #c9353f;
           }
@@ -250,7 +264,7 @@ const handleSeeAll = (data: any) => {
           }
         }
         &.change {
-          background-color: #f5f2e6;
+          background-color: var(--color-change-tag-bg);
           .dot {
             background-color: #edb82f;
           }
@@ -267,7 +281,6 @@ const handleSeeAll = (data: any) => {
           margin-right: 4px;
         }
         .text {
-          margin-top: 2px;
           font-size: 10px;
           font-weight: 600;
           color: #5bb462;
@@ -293,7 +306,7 @@ const handleSeeAll = (data: any) => {
       span {
         color: var(--color-neutral-2);
         font-size: 12px;
-        line-height: 16px;
+        line-height: 14px;
       }
       .font_family {
         font-size: 16px;
@@ -303,19 +316,22 @@ const handleSeeAll = (data: any) => {
       }
     }
     div.delay-time {
+      height: 19px;
       margin-top: 6px;
+      margin-bottom: -2px;
       span,
       .font_family {
         color: #c9353f;
       }
       span {
+        display: inline-block;
+        margin-top: 3px;
         line-height: 19px;
       }
       .font_family {
+        margin: 0 6px 0 -1px;
         line-height: 12px;
-        margin-left: -1px;
-        margin-right: 6px;
-        font-size: 19px;
+        font-size: 18px;
       }
     }
     div.change-time {
@@ -325,11 +341,10 @@ const handleSeeAll = (data: any) => {
       }
     }
     .previous {
-      height: 24px;
       margin-top: 8px;
       padding-left: 8px;
       line-height: 24px;
-      background-color: #e1e3e9;
+      background-color: var(--color-previous-bg);
       border-radius: 6px;
       span {
         font-size: 12px;

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

@@ -1,5 +1,10 @@
 <script setup lang="ts">
+import { useRouter } from 'vue-router'
+
+const router = useRouter()
+
 interface FeatureUpdateCardPropsData {
+  id: string
   title: string
   header: string
   content: string
@@ -10,6 +15,21 @@ interface FeatureUpdateCardPropsData {
 const props = defineProps<{
   data: FeatureUpdateCardPropsData
 }>()
+
+const emit = defineEmits<{
+  viewMore: []
+}>()
+const handleViewMore = () => {
+  router.push({
+    name: 'System Message Detail',
+    query: {
+      type: 'feature',
+      title: props.data.header,
+      id: props.data.id
+    }
+  })
+  emit('viewMore')
+}
 </script>
 
 <template>
@@ -28,10 +48,16 @@ const props = defineProps<{
         <div class="content-text">
           <span>{{ data.content }}</span>
         </div>
-        <img src="../images/test.png" style="margin: 12px 0 16px 24px; border-radius: 8px" alt="" />
+        <div class="feature-img">
+          <el-image :src="data.imgSrc" fit="contain" alt="feature-img"></el-image>
+        </div>
 
         <div class="change-btn" style="text-align: center">
-          <el-button class="el-button--main" style="height: 40px; padding: 0 32px">
+          <el-button
+            @click="handleViewMore"
+            class="el-button--main"
+            style="height: 40px; padding: 0 32px"
+          >
             View more</el-button
           >
         </div>
@@ -70,7 +96,12 @@ const props = defineProps<{
   }
   .card-content {
     padding: 16px 16px 24px 8px;
-    background: linear-gradient(137deg, #fff4eb 12.41%, #f0f3ff 52.63%, #e0f7f9 93.28%);
+    background: linear-gradient(
+      137deg,
+      var(--color-feature-card-first-bg) 12.41%,
+      var(--color-feature-card-second-bg) 52.63%,
+      var(--color-feature-card-third-bg) 93.28%
+    );
     border-radius: 12px;
     .title {
       img {
@@ -96,6 +127,18 @@ const props = defineProps<{
     .content-text {
       margin: 8px 0 0px 24px;
     }
+    .feature-img {
+      margin-top: 12px;
+      margin-bottom: 16px;
+      padding-left: 24px;
+      .el-image {
+        display: block;
+        margin: 0 auto;
+        width: 352px;
+        height: 200px;
+        border-radius: 8px;
+      }
+    }
   }
 }
 </style>

+ 24 - 6
src/components/NotificationMessageCard/src/components/PasswordCard.vue

@@ -1,14 +1,21 @@
 <script setup lang="ts">
+import ChangePasswordDialog from '@/views/Layout/src/components/Header/components/ChangePasswordDialog.vue'
+
 interface PasswordCardPropsData {
   title: string
-  isExpiration: boolean
-  tips: string
+  isExpiration: boolean // true为红色,false为绿色
+  header: string
   isRead: boolean
   content: string
 }
 const props = defineProps<{
   data: PasswordCardPropsData
 }>()
+
+const changePasswordDialogRef = ref()
+const handleChangePassword = () => {
+  changePasswordDialogRef.value.openDialog()
+}
 </script>
 
 <template>
@@ -20,18 +27,19 @@ const props = defineProps<{
     <div class="card-content" :class="{ 'is-expired': data.isExpiration }">
       <div class="title">
         <span class="font_family icon-icon_password_b"></span>
-        <span>{{ data.tips }}</span>
+        <span>{{ data.header }}</span>
       </div>
       <div class="details">
         {{ data.content }}
       </div>
       <div class="change-btn" style="text-align: center">
-        <el-button class="el-button--main" style="height: 40px">
+        <el-button @click="handleChangePassword" class="el-button--main" style="height: 40px">
           <span class="font_family icon-icon_edit_b" style="margin-right: 4px"></span>
           <span>Change Password</span></el-button
         >
       </div>
     </div>
+    <ChangePasswordDialog ref="changePasswordDialogRef"></ChangePasswordDialog>
   </div>
 </template>
 
@@ -65,15 +73,24 @@ const props = defineProps<{
   }
   .card-content {
     padding: 16px 8px 24px;
-    background: linear-gradient(180deg, #ffe294 0%, #f6f8fa 100%);
+    background: linear-gradient(
+      180deg,
+      var(--color-password-card-first-bg) 0%,
+      var(--color-password-card-second-bg) 100%
+    );
     border-radius: 12px;
     &.is-expired {
-      background: linear-gradient(182deg, #ef99a0 2.2%, #f6f8fa 98.77%);
+      background: linear-gradient(
+        182deg,
+        var(--color-password-expired-card-first-bg) 2.2%,
+        var(--color-password-expired-card-second-bg) 98.77%
+      );
     }
     .title {
       span {
         vertical-align: middle;
         font-weight: 700;
+        color: #2b2f36;
       }
       .font_family {
         margin-right: 8px;
@@ -81,6 +98,7 @@ const props = defineProps<{
     }
     .details {
       margin: 8px 0 16px 24px;
+      color: #2b2f36;
     }
   }
 }

二進制
src/components/NotificationMessageCard/src/images/test.png


+ 21 - 1
src/components/VSliderVerification/src/VSliderVerification.vue

@@ -34,6 +34,10 @@ const addTipsNode = () => {
   const childNode = document.createElement('div')
   childNode.className = 'tips'
   childNode.innerHTML = `
+    <div style="margin-bottom: 15px; text-align: right;">
+      <span class="font_family icon-icon_reject_b close-icon" style="margin-right: -20px;">
+      </span>
+    </div>
     <p>Please drag the slider below to complete the</p>
     <p>verification to ensure normal access</p>
   `
@@ -43,6 +47,14 @@ const addTipsNode = () => {
   } else {
     parentNode.appendChild(childNode)
   }
+  nextTick(() => {
+    const targetNode: any = document.querySelector('.font_family.icon-icon_reject_b.close-icon')
+    targetNode.onclick = closeDialog
+  })
+}
+
+const closeDialog = () => {
+  emit('close')
 }
 
 const styleMap = {
@@ -88,6 +100,7 @@ const updateSliderBackground = (state: string) => {
 
 const emit = defineEmits<{
   close: []
+  success: []
 }>()
 // 监听验证成功事件,因为这里库中的成功事件有0.8秒的延迟,所以这里手动监听验证成功事件
 const onSuccess = () => {
@@ -101,7 +114,7 @@ const onSuccess = () => {
             updateSliderBackground('success')
             addSliderBtnNode('success')
             setTimeout(() => {
-              emit('close')
+              emit('success')
               isShow.value = false
             }, 500)
           }
@@ -163,12 +176,19 @@ defineExpose({
   width: 400px;
   height: 373px;
   padding: 40px;
+  padding-top: 20px;
   background-color: var(--color-slider-bg);
   border-radius: 16px;
   box-shadow: -2px 2px 12px 0 rgba(0, 0, 0, 0.5);
   .tips {
     margin-bottom: 16px;
     text-align: center;
+    .close-icon {
+      cursor: pointer;
+      &:hover {
+        color: var(--color-theme);
+      }
+    }
   }
   .icon-border {
     display: flex;

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

@@ -0,0 +1,71 @@
+import { defineStore } from 'pinia'
+
+interface NotificationMessageState {
+  notificationMsgList: string[] // 页面中监听的未读消息id
+  readCardMap: string[] // 已读的消息id
+  hasNewMsg: boolean
+}
+
+export const useNotificationMessage = defineStore('notificationMessage', {
+  state: (): NotificationMessageState => ({
+    notificationMsgList: JSON.parse(localStorage.getItem('notificationMsgList')) || [],
+    readCardMap: JSON.parse(localStorage.getItem('readCardMap')) || [],
+    hasNewMsg: JSON.parse(localStorage.getItem('hasNewMsg')) || false
+  }),
+  getters: {},
+  actions: {
+    concatNotificationMsgList(array: any[]) {
+      this.notificationMsgList = [...new Set([...this.notificationMsgList, ...array])]
+      localStorage.setItem('notificationMsgList', JSON.stringify(this.notificationMsgList))
+    },
+    removeNotificationMsgList(array: any[]) {
+      this.notificationMsgList = this.notificationMsgList.filter((item) => !array.includes(item))
+
+      localStorage.setItem('notificationMsgList', JSON.stringify(this.notificationMsgList))
+    },
+    setReadCardMap(id: string) {
+      // 将页面中从未读到已读的消息id存入readCardMap
+      if (this.readCardMap.includes(id)) return
+      this.readCardMap.push(id)
+      localStorage.setItem('readCardMap', JSON.stringify(this.readCardMap))
+      // 将已读的消息从notificationMsgList中删除
+      this.notificationMsgList = this.notificationMsgList.filter((item) => {
+        return !this.readCardMap.includes(item)
+      })
+      localStorage.setItem('notificationMsgList', JSON.stringify(this.notificationMsgList))
+    },
+    hasUnreadMessages() {
+      $api.hasUnreadMessages().then((res) => {
+        if (res.code === 200) {
+          this.hasNewMsg = res.data.has_message
+          localStorage.setItem('hasNewMsg', JSON.stringify(this.hasNewMsg))
+        }
+      })
+    },
+    setHasNewMsg() {
+      this.hasNewMsg = true
+      localStorage.setItem('hasNewMsg', JSON.stringify(this.hasNewMsg))
+    },
+    async markMessageAsRead() {
+      if (this.readCardMap.length === 0) return
+
+      await $api.setMessageRead({ id: this.readCardMap }).then((res) => {
+        if (res.code === 200) {
+          this.readCardMap = []
+          localStorage.setItem('readCardMap', JSON.stringify(this.readCardMap))
+
+          // 在将消息标记为已读后,再次检查是否有新消息
+          this.hasUnreadMessages()
+        }
+      })
+    },
+    clearData() {
+      this.notificationMsgList = []
+      this.readCardMap = []
+      this.hasNewMsg = false
+      localStorage.removeItem('hasNewMsg')
+      localStorage.removeItem('notificationMsgList')
+      localStorage.removeItem('readCardMap')
+    }
+  }
+})

+ 2 - 0
src/stores/modules/user.ts

@@ -1,5 +1,6 @@
 import { defineStore } from 'pinia'
 import { useVisitedRowState } from './visitedRow'
+import { useNotificationMessage } from './notificationMessage'
 import dayjs from 'dayjs'
 
 interface UserInfo {
@@ -91,6 +92,7 @@ export const useUserStore = defineStore('user', {
       }
       this.isFirstLogin = false
       useVisitedRowState().clearVisitedRow()
+      useNotificationMessage().clearData()
     }
   }
 })

+ 1 - 1
src/styles/elementui.scss

@@ -338,7 +338,7 @@ div.el-drawer {
     height: 64px;
     padding: 16px;
     margin-bottom: 0;
-    background-color: var(--color-table-header-bg);
+    background-color: var(--color-header-bg);
     & > span {
       font-weight: 700;
       font-size: 24px;

+ 30 - 0
src/styles/theme.scss

@@ -256,6 +256,21 @@
   --color-upload-file-color: #b5b9bf;
   --color-upload-file-border-bg: #f5b279;
 
+  --color-personal-preference-bg: #f5f7fa;
+
+  --color-password-card-first-bg: #ffe294;
+  --color-password-card-second-bg: #f6f8fa;
+  --color-password-expired-card-first-bg: #ef99a0;
+  --color-password-expired-card-second-bg: #f6f8fa;
+  --color-feature-card-first-bg: #fff4eb;
+  --color-feature-card-second-bg: #f0f3ff;
+  --color-feature-card-third-bg: #f0f3ff;
+
+  --color-milestone-tag-bg: #e6f1eb;
+  --color-delay-tag-bg: #f7e7e9;
+  --color-change-tag-bg: #f5f2e6;
+
+  --color-previous-bg: #e1e3e9;
   --color-system-color-bg: #f6f8fa;
   --color-system-border: #eaebed;
   --color-system-checkbox-bg: #fff;
@@ -345,6 +360,21 @@
   --color-v-box-content-drag-bg: #2b2f36;
 
   --color-input-disabled-border: #656f7d;
+
+  --color-password-card-first-bg: #ffa000;
+  --color-password-card-second-bg: #d6c587;
+  --color-password-expired-card-first-bg: #f25a66;
+  --color-password-expired-card-second-bg: #cda9ac;
+  --color-feature-card-first-bg: #334181;
+  --color-feature-card-second-bg: #c651bd;
+  --color-feature-card-third-bg: #ffca6e;
+  --color-milestone-tag-bg: #3c5249;
+  --color-delay-tag-bg: #523942;
+  --color-change-tag-bg: #564f36;
+
+  --color-previous-bg: #454b54;
+
+  --color-system-message-nav-bg: #343a43;
   // 邮件
   --w-e-toolbar-bg-color: var(--color-email-bg);
   --w-e-textarea-bg-color: var(--color-email-bg);

+ 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) {

+ 2 - 2
src/views/Dashboard/src/components/RecentStatus.vue

@@ -83,7 +83,7 @@ const SubscribeShipments = (val: any) => {
           <span>{{ item.bookingNumber }}</span>
         </div>
       </div>
-      <!-- <div class="recent-header-right">
+      <div class="recent-header-right">
         <el-button
           class="recent_button"
           @click="SubscribeShipments(item)"
@@ -101,7 +101,7 @@ const SubscribeShipments = (val: any) => {
           </span>
           <span class="Subscribe">Subscribe</span>
         </el-button>
-      </div> -->
+      </div>
     </div>
     <div class="recent_content">
       <!-- 左 -->

+ 16 - 4
src/views/Layout/src/components/Header/HeaderView.vue

@@ -8,15 +8,19 @@ import { useHeaderSearch } from '@/stores/modules/headerSearch'
 import { onBeforeRouteUpdate } from 'vue-router'
 import { useLoadingState } from '@/stores/modules/loadingState'
 import { useThemeStore } from '@/stores/modules/theme'
+import { useNotificationMessage } from '@/stores/modules/notificationMessage'
 import NotificationDrawer from './components/NotificationDrawer.vue'
 import TrainingCard from './components/TrainingCard.vue'
 
+const notificationMsgStore = useNotificationMessage()
 const themeStore = useThemeStore()
 const userStore = useUserStore()
 const route = useRoute()
 const router = useRouter()
 const headerSearch = useHeaderSearch()
 
+const trainingCardRef = ref()
+
 // 切换系统主题颜色
 const toggleThemeMode = (theme: string) => {
   themeStore.toggleTheme(theme, true)
@@ -177,6 +181,14 @@ const closePopover = () => {
 }
 
 const notificationDrawer = ref(false)
+
+onBeforeRouteUpdate((to, from, next) => {
+  if (from.name === 'Login' && userStore.userName) {
+    notificationMsgStore.hasUnreadMessages()
+  }
+
+  next()
+})
 </script>
 
 <template>
@@ -188,7 +200,7 @@ const notificationDrawer = ref(false)
     element-loading-background="rgb(43, 47, 54, 0.7)"
   >
     <VBreadcrumb></VBreadcrumb>
-    <TrainingCard></TrainingCard>
+    <TrainingCard ref="trainingCardRef"></TrainingCard>
     <div class="right-info">
       <el-input
         v-model="searchValue"
@@ -200,8 +212,8 @@ const notificationDrawer = ref(false)
           <span style="margin-top: -1px" class="font_family icon-icon_search_b"></span>
         </template>
       </el-input>
-      <!-- <div class="notice-icon" v-if="userStore.userInfo?.uname">
-        <span class="unread-tip-icon"></span>
+      <div class="notice-icon" v-if="userStore.userInfo?.uname">
+        <span v-if="notificationMsgStore.hasNewMsg" class="unread-tip-icon"></span>
         <el-button
           style="height: 40px; width: 40px; margin-right: 0px"
           class="el-button--text"
@@ -209,7 +221,7 @@ const notificationDrawer = ref(false)
         >
           <span class="font_family icon-icon_notice_b" style="font-size: 18px"></span>
         </el-button>
-      </div> -->
+      </div>
       <!-- 
       <span class="font_family icon-icon_language_b" style="font-size: 16px"></span> -->
       <el-popover

+ 47 - 80
src/views/Layout/src/components/Header/components/NotificationDrawer.vue

@@ -1,11 +1,12 @@
 <script setup lang="ts">
 import NotificationMessageCard from '@/components/NotificationMessageCard/src/NotificationMessageCard.vue'
 import { useRouter } from 'vue-router'
+import { useNotificationMessage } from '@/stores/modules/notificationMessage'
 
 const router = useRouter()
-const loading = ref(false)
+const notificationMsgStore = useNotificationMessage()
 
-const drawerModel = defineModel('drawerModel', { type: Boolean, default: false })
+const loading = ref(false)
 
 const notificationType = ref('all')
 const notificationTypeList = ref({
@@ -19,78 +20,19 @@ const notificationTypeList = ref({
 })
 
 const notificationList = ref<any[]>([])
-// const notificationList = [
-//   {
-//     notificationType: 'feature',
-//     info: {
-//       isRead: true,
-//       title: 'Feature Update',
-//       header: 'New feature online: Quick search has been released!',
-//       content:
-//         'We are pleased to announce that the quick search function is now officially online! You can now quickly find what you need by entering keywords, greatly improving your work efficiency. Go and experience it!'
-//     }
-//   },
-//   {
-//     notificationType: 'event',
-//     info: {
-//       type: 'milestone',
-//       isMultiple: true,
-//       numericRecords: 3,
-//       isRead: true,
-//       title: 'Milestone Update',
-//       mode: 'Ocean Freight',
-//       no: 'HBOL: SHJN2301234',
-//       tag: 'Booking Confirmed',
-//       location: 'Hong Kong',
-//       time: 'Jan 10, 2025 14:30 UTC+8',
-//       info: {
-//         route: ['Hong Kong', 'Shanghai', 'Ningbo']
-//       }
-//     }
-//   },
-//   {
-//     notificationType: 'password',
-//     info: {
-//       title: 'Password Notifications',
-//       isExpiration: true,
-//       tips: 'Password Expiration in 311 Days',
-//       content:
-//         'Your password will expire in 7 days. To ensure the security of your account, please change your password as soon as possible.'
-//     }
-//   },
-//   {
-//     notificationType: 'password',
-//     info: {
-//       isRead: false,
-//       title: 'Password Notifications',
-//       isExpiration: false,
-//       tips: 'Password Expiration in 3 Days',
-//       content:
-//         'Your password will expire in 7 days. To ensure the security of your account, please change your password as soon as possible.'
-//     }
-//   },
-//   {
-//     notificationType: 'password',
-//     info: {
-//       isRead: true,
-//       title: 'Password Notifications',
-//       isExpiration: true,
-//       tips: 'Password Expiration in 31111 Days',
-//       content:
-//         'Your password will expire in 7 days. To ensure the security of your account, please change your password as soon as possible.'
-//     }
-//   }
-// ]
 
 const getNotificationList = () => {
   loading.value = true
   $api
     .getNotificationList({
-      rules_type: 'Milestone_Update'
+      rules_type: notificationType.value
     })
     .then((res) => {
       if (res.code === 200) {
         notificationList.value = res.data
+        // nextTick(() => {
+        //   init()
+        // })
       }
     })
     .finally(() => {
@@ -98,37 +40,55 @@ const getNotificationList = () => {
     })
 }
 
+// 标记所有为已读
 const handleMarkAllRead = () => {
-  // 标记所有为已读
-  // notificationList.value.forEach((item) => {
-  //   item.isRead = true
-  // })
+  try {
+    $api.setMessageRead({ read_type: true })
+  } catch (error) {
+    console.error(error)
+  }
 }
 
+const drawerRef = ref()
 const handleViewAll = () => {
+  drawerRef.value.handleClose()
   router.push('/system-message')
 }
 
 const handleSettingMessage = () => {
-  // 跳转消息设置页面
-  // router.push('/')
+  drawerRef.value.handleClose()
+  router.push({
+    name: 'Monitoring Settings',
+    query: {
+      tab: 'Subscribe Notifications'
+    }
+  })
 }
 
-const clearData = () => {
+const closeDrawer = () => {
   notificationList.value = []
+  notificationType.value = 'all'
+  notificationMsgStore.markMessageAsRead()
 }
+
+const notificationListRef = ref<HTMLElement | null>(null)
 </script>
 
 <template>
   <el-drawer
+    ref="drawerRef"
     @open="getNotificationList"
-    @closed="clearData"
+    @closed="closeDrawer"
     class="notice-drawer"
-    v-model="drawerModel"
     size="432px"
   >
     <template #header>
-      <el-select size="large" v-model="notificationType" class="notification-type">
+      <el-select
+        size="large"
+        @change="getNotificationList"
+        v-model="notificationType"
+        class="notification-type"
+      >
         <el-option
           v-for="(label, value) in notificationTypeList"
           :key="value"
@@ -138,7 +98,7 @@ const clearData = () => {
       </el-select>
     </template>
     <template #default>
-      <div v-vloading="loading" style="height: 100%">
+      <el-scrollbar v-vloading="loading" style="height: 100%">
         <div class="notification-header">
           <el-button @click="handleMarkAllRead" class="mark-all-read el-button--text" size="small">
             <span class="font_family icon-icon_confirm_b show-icon"></span>
@@ -162,10 +122,15 @@ const clearData = () => {
             </el-button>
           </div>
         </div>
-        <div class="notification-content">
-          <NotificationMessageCard :data="notificationList" />
+        <div class="notification-content" ref="notificationListRef">
+          <NotificationMessageCard
+            @see-all="drawerRef.handleClose()"
+            @view-more="drawerRef.handleClose()"
+            @jump-tracking="drawerRef.handleClose()"
+            :data="notificationList"
+          />
         </div>
-      </div>
+      </el-scrollbar>
     </template>
   </el-drawer>
 </template>
@@ -173,6 +138,7 @@ const clearData = () => {
 div.layout-toolbar {
   .notification-content {
     padding: 16px;
+    background-color: var(--color-dialog-body-bg);
   }
   .password-notifications {
     margin-bottom: 16px;
@@ -285,8 +251,9 @@ div.layout-toolbar {
       top: 0;
       height: 40px;
       padding: 0 16px;
+      z-index: 3;
       line-height: 40px;
-      background-color: white;
+      background-color: var(--color-dialog-body-bg);
       border-bottom: 1px solid var(--color-border);
       .mark-all-read {
         span {

+ 90 - 125
src/views/Layout/src/components/Header/components/TrainingCard.vue

@@ -1,163 +1,124 @@
 <script setup lang="ts">
+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 notificationList = []
-// const notificationList = [
-//   {
-//     notificationType: 'feature',
-//     info: {
-//       title: 'Feature Update',
-//       header: 'New feature online: Quick search has been released!',
-//       content:
-//         'We are pleased to announce that the quick search function is now officially online! You can now quickly find what you need by entering keywords, greatly improving your work efficiency. Go and experience it!'
-//     }
-//   },
-//   {
-//     notificationType: 'event',
-//     info: {
-//       type: 'milestone',
-//       isMultiple: true,
-//       numericRecords: 3,
-//       isRead: true,
-//       title: 'Milestone Update',
-//       mode: 'Ocean Freight',
-//       no: 'HBOL: SHJN2301234',
-//       tag: 'Booking Confirmed',
-//       location: 'Hong Kong',
-//       time: 'Jan 10, 2025 14:30 UTC+8'
-//     }
-//   },
-//   {
-//     notificationType: 'password',
-//     info: {
-//       title: 'Password Notifications',
-//       isExpiration: true,
-//       tips: 'Password Expiration in 311 Days',
-//       content:
-//         'Your password will expire in 7 days. To ensure the security of your account, please change your password as soon as possible.'
-//     }
-//   },
-//   {
-//     notificationType: 'password',
-//     info: {
-//       title: 'Password Notifications',
-//       isExpiration: false,
-//       tips: 'Password Expiration in 3 Days',
-//       content:
-//         'Your password will expire in 7 days. To ensure the security of your account, please change your password as soon as possible.'
-//     }
-//   },
-//   {
-//     notificationType: 'password',
-//     info: {
-//       title: 'Password Notifications',
-//       isExpiration: true,
-//       tips: 'Password Expiration in 31111 Days',
-//       content:
-//         'Your password will expire in 7 days. To ensure the security of your account, please change your password as soon as possible.'
-//     }
-//   },
-//   {
-//     notificationType: 'event',
-//     info: {
-//       type: 'milestone',
-//       isMultiple: true,
-//       numericRecords: 3,
-//       isRead: true,
-//       title: 'Milestone Update 1',
-//       mode: 'Ocean Freight',
-//       no: 'HBOL: SHJN2301234',
-//       tag: 'Booking Confirmed',
-//       location: 'Hong Kong',
-//       time: 'Jan 10, 2025 14:30 UTC+8'
-//     }
-//   },
-//   {
-//     notificationType: 'event',
-//     info: {
-//       type: 'milestone',
-//       isMultiple: true,
-//       numericRecords: 3,
-//       isRead: true,
-//       title: 'Milestone Update 2',
-//       mode: 'Ocean Freight',
-//       no: 'HBOL: SHJN2301234',
-//       tag: 'Booking Confirmed',
-//       location: 'Hong Kong',
-//       time: 'Jan 10, 2025 14:30 UTC+8'
-//     }
-//   },
-//   {
-//     notificationType: 'event',
-//     info: {
-//       type: 'milestone',
-//       isMultiple: true,
-//       numericRecords: 3,
-//       isRead: true,
-//       title: 'Milestone Update 3',
-//       mode: 'Ocean Freight',
-//       no: 'HBOL: SHJN2301234',
-//       tag: 'Booking Confirmed',
-//       location: 'Hong Kong',
-//       time: 'Jan 10, 2025 14:30 UTC+8'
-//     }
-//   }
-// ]
-
-const getNotificationList = () => {
-  if (localStorage.getItem('showFeatureAfterLogin') !== 'true') return
-  // 获取数据
-
-  localStorage.removeItem('showFeatureAfterLogin')
-}
+const userStore = useUserStore()
+const notificationMsgStore = useNotificationMessage()
+
+const notificationList = ref([])
 
 const curCard = computed(() => {
-  return notificationList[curIndex.value] || null
+  return notificationList.value[curIndex.value] || null
 })
 const curIndex = ref(0)
 // 设置定时器进行自动轮播
-let intervalId = null
+let trainingIntervalId = null
+
+// 轮询最新未读消息
+let newMessageIntervalId = null
+// 轮询最新未读消息
+const pollingNewMessage = () => {
+  // 每隔5分钟轮询一次
+  newMessageIntervalId = setInterval(() => {
+    getNotificationList(dayjs().format('MM/DD/YYYY HH:mm:ss'))
+  }, 300000)
+}
+pollingNewMessage()
+
+// 登录后自动轮播消息
+const trainingCardAfterLogin = () => {
+  curIndex.value = curIndex.value + 1
+  // 如果消息展示完毕,清除定时器
+  if (curIndex.value >= notificationList.value.length) {
+    clearInterval(trainingIntervalId)
+  }
+}
 
 const nextNotification = () => {
   let result = true
   // 更新当前索引和卡片
   curIndex.value = curIndex.value + 1
-  // curCard.value = notificationList[curIndex.value]
   // 如果消息为password或者feature类型,暂停自动轮播
   if (
     curCard.value?.notificationType === 'password' ||
     curCard.value?.notificationType === 'feature'
   ) {
-    clearInterval(intervalId)
+    clearInterval(trainingIntervalId)
     result = false
   }
-
   // 如果到达最后一个消息,设置为null以清除显示
-  if (curIndex.value >= notificationList.length) {
-    clearInterval(intervalId)
-    // curCard.value = null
+  if (curIndex.value >= notificationList.value.length) {
+    clearInterval(trainingIntervalId)
     result = false
   }
   return result
 }
 
+// 轮询时的轮播定时器
 const initTrainingCard = () => {
   if (curCard.value?.notificationType === 'event') {
-    intervalId = setInterval(nextNotification, 2000)
+    trainingIntervalId = setInterval(nextNotification, 2000)
   }
 }
-initTrainingCard()
 
+onBeforeRouteUpdate((to, from, next) => {
+  if (from.name === 'Login' && userStore.userName) {
+    getNotificationList()
+  }
+  if (to.name === 'Login') {
+    clearInterval(trainingIntervalId)
+    clearInterval(newMessageIntervalId)
+    newMessageIntervalId = null
+  }
+  next()
+})
+
+const getNotificationList = (time?: string) => {
+  $api
+    .getNotificationList({
+      rules_type: 'all',
+      info_type: true,
+      current_time: time
+    })
+    .then((res) => {
+      if (res.code === 200) {
+        const data = res.data
+        curIndex.value = 0
+        notificationList.value = data
+
+        data.length > 0 && notificationMsgStore.setHasNewMsg()
+
+        if (time && data.length > 0) {
+          initTrainingCard()
+        } else {
+          trainingIntervalId = setInterval(trainingCardAfterLogin, 2000)
+        }
+      }
+    })
+}
+
+// 关闭消息,如果是登录后自动轮播的消息,则需清除定时器,如果不是则继续轮播下一条消息
 const closeMessage = () => {
+  if (!newMessageIntervalId) {
+    clearInterval(trainingIntervalId)
+    curIndex.value = -1
+    return
+  }
+
   // 如果当前消息为event类型,则需先清除定时器
   if (curCard.value?.notificationType === 'event') {
-    clearInterval(intervalId)
+    clearInterval(trainingIntervalId)
   }
 
   const result = nextNotification()
   if (result) {
-    intervalId = setInterval(nextNotification, 2000)
+    trainingIntervalId = setInterval(nextNotification, 2000)
   }
+  // 将当前消息标记为已读
+  // notificationMsgStore.setReadCardMap(curCard.value?.info?.id)
 }
 </script>
 
@@ -170,7 +131,11 @@ const closeMessage = () => {
     >
       <span class="font_family icon-icon_reject_b"></span>
     </el-button>
-    <NotificationMessageCard v-if="curCard" :data="[curCard]"></NotificationMessageCard>
+    <NotificationMessageCard
+      :isObserver="false"
+      v-if="curCard"
+      :data="[curCard]"
+    ></NotificationMessageCard>
   </div>
 </template>
 
@@ -179,13 +144,13 @@ const closeMessage = () => {
   position: absolute;
   top: 60px;
   right: 20px;
-  z-index: 2010;
+  z-index: 2300;
   width: 432px;
   padding: 16px;
   padding-bottom: 0;
-  background-color: #fff;
+  background-color: var(--color-dialog-body-bg);
   border-radius: 12px;
-  box-shadow: 4px 4px 16px 0px rgba(0, 0, 0, 0.1);
+  box-shadow: 4px 4px 32px 0px rgba(0, 0, 0, 0.2);
   .close-icon {
     position: absolute;
     top: 14px;

+ 0 - 43
src/views/Layout/src/components/Menu/MenuView.vue

@@ -22,49 +22,6 @@ const getMenuList = () => {
       menuList.value = res.data
     }
   })
-  // menuList.value = [
-  //   {
-  //     index: '1',
-  //     label: 'Dashboard',
-  //     icon: 'icon_data_fill_b',
-  //     path: '/dashboard'
-  //   },
-  //   {
-  //     index: '2',
-  //     label: 'Booking',
-  //     icon: 'icon_booking__fill_b',
-  //     path: '/booking'
-  //   },
-  //   {
-  //     index: '3',
-  //     label: 'Tracking',
-  //     icon: 'icon_tracking__fill_b',
-  //     path: '/tracking'
-  //   },
-  //   {
-  //     index: '4',
-  //     label: 'System Management',
-  //     icon: 'icon_system__management_fill_b',
-  //     type: 'list',
-  //     children: [
-  //       {
-  //         index: '4-1',
-  //         label: 'System Message',
-  //         path: '/system-message'
-  //       },
-  //       {
-  //         index: '4-2',
-  //         label: 'System Settings',
-  //         path: '/SystemSettings'
-  //       },
-  //       {
-  //         index: '4-3',
-  //         label: 'Operation Log',
-  //         path: '/Operationlog'
-  //       }
-  //     ]
-  //   }
-  // ]
 }
 getMenuList()
 //监听窗口大小

+ 2 - 1
src/views/Login/src/loginView.vue

@@ -454,7 +454,8 @@ const firstLoginTipsRef = ref()
     </el-card>
     <VSliderVerification
       v-if="isShowSliderVerification"
-      @close="confirmVerification"
+      @success="confirmVerification"
+      @close="isShowSliderVerification = false"
       ref="sliderVerificationRef"
     ></VSliderVerification>
     <ErrorTips ref="errorTipsRef" @forget-password="status = 'reset'"></ErrorTips>

+ 180 - 220
src/views/SystemMessage/src/SystemMessage.vue

@@ -1,243 +1,203 @@
 <script setup lang="ts">
-import EventCard from '@/components/NotificationMessageCard/src/components/EventCard.vue'
+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 navList = [
-  {
-    title: 'Milestone Update',
-    count: 2
-  },
-  {
-    title: 'Container Status Update',
-    count: 1
-  },
-  {
-    title: 'Departure/Arrival Delay',
-    count: '99+'
-  },
-  {
-    title: 'ETD/ETA Change',
-    count: 0
+const tabCountList = ref([0, 0, 0, 0, 0])
+const curTabCount = ref([])
+
+const handleCount = (count: number) => {
+  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',
+  'Departure/Arrival Delay',
+  'ETD/ETA Change'
 ]
 
-const activeItem = ref('Milestone Update')
+const notificationTypeList = ref({
+  Milestone_Update: 'Milestone Update',
+  Container_Status_Update: 'Container Status Update',
+  Container_Arrival: 'Container Arrival',
+  'Departure/Arrival_Delay': 'Departure/Arrival Delay',
+  'ETD/ETA_Change': 'ETD/ETA Change',
+  Feature_Update: 'Feature Update'
+})
+
 const setActiveItem = (item: string) => {
-  activeItem.value = item
-}
+  navList.forEach((navItem, index) => {
+    curTabCount.value[index] = handleShowCount(navItem, index)
+  })
+  curTabCount.value[tabCountList.value.length - 1] = handleShowCount(
+    'Feature Update',
+    tabCountList.value.length - 1
+  )
 
-// const getNotificationList = () => {
-//   $api
-//     .getNotificationList({
-//       rules_type: 'Milestone_Update'
-//     })
-//     .then((res) => {
-//       if (res.code === 200) {
-//         notificationList.value = res.data.info
-//         console.log(res, 'test')
-//       }
-//     })
-// }
+  activeCardTypeName.value = item
+  sessionStorage.setItem('activeCardTypeName', item)
+  activeTabName.value = 'All Notifications'
+  getNotificationList()
+}
 
-const activeName = ref('first')
+const loading = ref(false)
+const notificationList = ref<any[]>([])
 
-const handleClick = () => {}
+const unreadNotificationList = computed(() => {
+  return notificationList.value.filter((item) => !item.info.isRead)
+})
+const readNotificationList = computed(() => {
+  return notificationList.value.filter((item) => item.info.isRead)
+})
+const getNotificationList = async () => {
+  loading.value = true
+  const rulesType = Object.entries(notificationTypeList.value).find(
+    (item) => item[1] === activeCardTypeName.value
+  )?.[0]
+  try {
+    await notificationMsgStore.markMessageAsRead()
+    $api
+      .getSystemMessageData({
+        rules_type: rulesType
+      })
+      .then((res) => {
+        if (res.code === 200) {
+          const data = res.data
+          notificationList.value = data.cardList
+          tabCountList.value = data.countList
+        }
+      })
+      .finally(() => {
+        loading.value = false
+        curTabCount.value = []
+      })
+  } catch (error) {
+    console.error(error)
+    loading.value = false
+    curTabCount.value = []
+  }
+}
 
-// const notificationList = ref<any[]>([])
-const notificationList = [
-  {
-    type: 'milestone',
-    isMultiple: true,
-    numericRecords: 3,
-    isRead: true,
-    title: 'Milestone Update',
-    mode: 'Ocean Freight',
-    no: 'SHJN2301234',
-    tag: 'Booking Confirmed',
-    location: 'Hong Kong',
-    timeLabel: 'ATA: ',
-    time: '2510-12-1 14:30 UTC+8',
-    timezone: 'Asia/Shanghai'
-  },
-  {
-    type: 'container',
-    isRead: false,
-    mode: '',
-    no: 'SHJN2301234',
-    tag: 'Unloaded From Vessel',
-    location: 'Hong Kong',
-    timeLabel: 'ATA: ',
-    time: '2510-12-1 14:30 UTC+8',
-    timezone: 'Asia/Shanghai',
-    previous: 'Previous: Departure from Shanghai (08:15 UTC+8)'
-  },
-  {
-    type: 'delay',
-    numericRecords: 0,
-    isRead: false,
-    title: 'Delay Daily Summary (Jan 10, 2025)',
-    mode: 'Air Freight',
-    no: 'SHJN2301234',
-    tag: 'Departure Delay',
-    location: 'Hong Kong',
-    timeLabel: 'ATA: ',
-    time: '2510-12-1 14:30 UTC+8',
-    timezone: 'Asia/Shanghai',
-    info: {
-      timeLabel: 'ATA: ',
-      time: '2510-12-1 14:30 UTC+8',
-      timezone: 'Asia/Shanghai',
-      departureDelayNum: 10,
-      arrivalDelayNum: 8
+const changeCardRead = () => {
+  const readCardMap = notificationMsgStore.readCardMap
+  notificationList.value.forEach((item) => {
+    if (readCardMap.includes(item.info.id)) {
+      item.info.isRead = true
     }
-  },
-  {
-    type: 'container',
-    isRead: false,
-    mode: '',
-    no: 'SHJN2301234',
-    tag: 'Unloaded From Vessel',
-    location: 'Hong Kong',
-    timeLabel: 'ATA: ',
-    time: '2510-12-1 14:30 UTC+8',
-    timezone: 'Asia/Shanghai',
-    previous: 'Previous: Departure from Shanghai (08:15 UTC+8)'
-  },
-  {
-    type: 'delay',
-    numericRecords: 0,
-    isRead: false,
-    title: 'Delay Daily Summary (Jan 10, 2025)',
-    mode: 'Air Freight',
-    no: 'SHJN2301234',
-    tag: 'Departure Delay',
-    location: 'Hong Kong',
-    timeLabel: 'ATA: ',
-    time: '2510-12-1 14:30 UTC+8',
-    timezone: 'Asia/Shanghai',
-    info: {
-      timeLabel: 'ATA: ',
-      time: '2510-12-1 14:30 UTC+8',
-      timezone: 'Asia/Shanghai',
-      departureDelayNum: 10,
-      arrivalDelayNum: 8
-    }
-  },
-  {
-    type: 'container',
-    isRead: false,
-    mode: '',
-    no: 'SHJN2301234',
-    tag: 'Unloaded From Vessel',
-    location: 'Hong Kong',
-    timeLabel: 'ATA: ',
-    time: '2510-12-1 14:30 UTC+8',
-    timezone: 'Asia/Shanghai',
-    previous: 'Previous: Departure from Shanghai (08:15 UTC+8)'
-  },
-  {
-    type: 'delay',
-    numericRecords: 0,
-    isRead: false,
-    title: 'Delay Daily Summary (Jan 10, 2025)',
-    mode: 'Air Freight',
-    no: 'SHJN2301234',
-    tag: 'Departure Delay',
-    location: 'Hong Kong',
-    timeLabel: 'ATA: ',
-    time: '2510-12-1 14:30 UTC+8',
-    timezone: 'Asia/Shanghai',
-    info: {
-      timeLabel: 'ATA: ',
-      time: '2510-12-1 14:30 UTC+8',
-      timezone: 'Asia/Shanghai',
-      departureDelayNum: 10,
-      arrivalDelayNum: 8
+  })
+}
+
+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
     }
-  },
-  {
-    type: 'change',
-    numericRecords: 0,
-    isRead: false,
-    title: 'ETD/ETA  Change Weekly Summary (Jan 4- 10, 2025) ',
-    mode: 'Air Freight',
-    no: 'SHJN2301234',
-    tag: 'ETD Change',
-    info: {
-      etdChangeNum: 20,
-      etaChangeNum: 10,
-      timeLabel: 'ATA: ',
-      time: '2510-12-1 14:30 UTC+8',
-      timezone: 'Asia/Shanghai'
-    },
-    location: 'Hong Kong',
-    changeTime: 'Updated ETD: Jan 17, 15:00',
-    timeLabel: 'ATA: ',
-    time: '2510-12-1 14:30 UTC+8',
-    timezone: 'Asia/Shanghai'
-  }
-]
-// onMounted(() => {
-//   getNotificationList()
-// })
+  })
+}
+
+onMounted(() => {
+  getNotificationList()
+})
 </script>
 
 <template>
-  <div class="Title">System Message</div>
-  <div class="system-message">
-    <div class="left-nav">
-      <el-collapse v-model="collapseVModel">
-        <el-collapse-item title="Event Notifications" name="1">
-          <div
-            @click="setActiveItem(item.title)"
-            class="collapse-item"
-            :class="{ 'is-active': item.title === activeItem }"
-            v-for="item in navList"
-            :key="item.title"
-          >
-            <div v-if="item.title === activeItem" class="active-sign"></div>
-            <span>{{ item.title }}</span>
-            <div class="count" v-if="item.count">
-              <span>{{ item.count }}</span>
+  <div v-vloading="loading" style="height: 100%; width: 100%">
+    <div class="Title">System Message</div>
+    <div class="system-message">
+      <div class="left-nav">
+        <el-collapse v-model="collapseVModel">
+          <el-collapse-item title="Event Notifications" name="1">
+            <div
+              @click="setActiveItem(item)"
+              class="collapse-item"
+              :class="{ 'is-active': item === activeCardTypeName }"
+              v-for="(item, index) in navList"
+              :key="item"
+            >
+              <div v-if="item === activeCardTypeName" class="active-sign"></div>
+              <span>{{ item }}</span>
+              <div class="count" v-if="handleShowCount(item, index)">
+                <span>{{ handleShowCount(item, index) }}</span>
+              </div>
             </div>
+          </el-collapse-item>
+        </el-collapse>
+        <div
+          @click="setActiveItem('Feature Update')"
+          class="collapse-item"
+          style="margin-top: 4px; font-weight: 700"
+          :class="{ 'is-active': activeCardTypeName === 'Feature Update' }"
+        >
+          <div v-if="activeCardTypeName === 'Feature Update'" class="active-sign"></div>
+          <span>Feature Update</span>
+          <div class="count" v-if="handleShowCount('Feature Update', tabCountList.length - 1)">
+            <span>{{ handleShowCount('Feature Update', tabCountList.length - 1) }}</span>
           </div>
-        </el-collapse-item>
-      </el-collapse>
-      <div
-        @click="setActiveItem('Feature Update')"
-        class="collapse-item"
-        style="margin-top: 4px; font-weight: 700"
-        :class="{ 'is-active': activeItem === 'Feature Update' }"
-      >
-        <div v-if="activeItem === 'Feature Update'" class="active-sign"></div>
-        <span>Feature Update</span>
-        <div class="count">
-          <span>33</span>
         </div>
       </div>
-    </div>
-    <div class="right-content">
-      <el-tabs v-model="activeName" class="demo-tabs" @tab-click="handleClick">
-        <el-tab-pane label="All Notifications" name="first">
-          <div style="padding: 10px 140px 0px 16px">
-            <EventCard
-              v-for="(item, index) in notificationList"
-              :key="index"
-              :data="item"
-            ></EventCard>
-          </div>
-        </el-tab-pane>
-        <el-tab-pane label="Unread" name="second">
-          <template #label>
-            <span style="margin-right: 4px">Unread</span>
-            <div class="count">
-              <span>33</span>
+      <div class="right-content">
+        <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="activeTabName === 'All Notifications'">
+              <NotificationMessageCard
+                v-if="activeTabName === 'All Notifications'"
+                :data="notificationList"
+                @hasCardRead="changeCardRead"
+                :updateReadCardsOnChange="false"
+              ></NotificationMessageCard>
             </div>
-          </template>
-        </el-tab-pane>
-        <el-tab-pane label="Read" name="third">Role</el-tab-pane>
-      </el-tabs>
+          </el-tab-pane>
+          <el-tab-pane label="Unread" name="Unread">
+            <template #label>
+              <span style="margin-right: 4px">Unread</span>
+              <div class="count" v-if="unreadNotificationList.length">
+                <span>{{ handleCount(unreadNotificationList.length) }}</span>
+              </div>
+            </template>
+            <div style="padding: 10px 140px 0px 16px" v-if="activeTabName === 'Unread'">
+              <NotificationMessageCard
+                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="activeTabName === 'Read'">
+              <NotificationMessageCard
+                v-if="activeTabName === 'Read'"
+                :updateReadCardsOnChange="false"
+                :data="readNotificationList"
+              >
+              </NotificationMessageCard>
+            </div>
+          </el-tab-pane>
+        </el-tabs>
+      </div>
     </div>
   </div>
 </template>
@@ -257,10 +217,10 @@ const notificationList = [
   height: calc(100% - 68px);
   .count {
     display: inline-flex;
-    justify-content: center;
     height: 18px;
     min-width: 18px;
-    padding-left: 4px;
+    padding-top: 1px;
+    padding-left: 5px;
     padding-right: 5px;
     background-color: var(--color-theme);
     border-radius: 9px;

+ 111 - 45
src/views/SystemMessage/src/components/SystemMessageDetail.vue

@@ -1,76 +1,139 @@
 <script setup lang="ts">
+import { useRoute } from 'vue-router'
 import EventCard from '@/components/NotificationMessageCard/src/components/EventCard.vue'
+import { useNotificationMessage } from '@/stores/modules/notificationMessage'
 
-const notificationData: any = {
-  title: 'Milestone Update Daily Summary (Jan 10, 2025)',
-  numericRecords: 3,
-  notificationList: [
-    {
-      mode: 'Ocean Freight',
-      no: 'HBOL: SHJN2301234',
-      tag: 'Booking Confirmed',
-      location: 'Hong Kong',
-      timeLabel: 'ATA: ',
-      time: '2510-12-1 14:30 UTC+8',
-      timezone: 'Asia/Shanghai'
-    },
-    {
-      mode: 'Air Freight',
-      no: 'HBOL: SHJN2301234',
-      tag: 'Booking Confirmed',
-      location: 'Hong Kong',
-      timeLabel: 'ATA: ',
-      time: '2510-12-1 14:30 UTC+8',
-      timezone: 'Asia/Shanghai'
-    },
-    {
-      mode: 'Air Freight',
-      no: 'HBOL: SHJN2301234',
-      tag: 'Booking Confirmed',
-      location: 'Hong Kong',
-      timeLabel: 'ATA: ',
-      time: '2510-12-1 14:30 UTC+8',
-      timezone: 'Asia/Shanghai',
-      previous: 'Previous: Departure from Shanghai (08:15 UTC+8)'
-    }
-  ]
+const route = useRoute()
+const notificationMsgStore = useNotificationMessage()
+const notificationData = ref({
+  title: '',
+  type: 'delay',
+  etdOrdeparturNum: -1,
+  etaOrarrivalNum: -1,
+  numericRecords: -1,
+  notificationList: []
+})
+
+const loading = ref(false)
+if (route.query.type === 'feature') {
+  loading.value = true
+}
+const getNotificationList = async () => {
+  await $api
+    .getNotificationDetails({
+      rules_type: route.query.rules_type,
+      frequency_type: route.query.frequency_type,
+      insert_date_format: route.query.insert_date_format
+    })
+    .then((res) => {
+      if (res.code === 200) {
+        notificationData.value = res.data
+        notificationMsgStore.hasUnreadMessages()
+      }
+    })
+    .finally(() => {
+      loading.value = false
+    })
 }
 
-const getNotificationList = () => {
-  $api.getNotificationDetails().then((res) => {
-    if (res.code === 200) {
-      // console.log(res, 'test')
-    }
-  })
+const iframeUrl = ref()
+const init = async () => {
+  loading.value = true
+  if (route.query.type === 'feature') {
+    notificationData.value.title = (route.query.title as string) || ''
+    iframeUrl.value = `${import.meta.env.VITE_API_HOST}/main_new_version.php?action=feature_update&id=${route.query.id}`
+  } else {
+    await getNotificationList()
+  }
 }
-onMounted(() => {
-  getNotificationList()
+// onMounted(() => {
+//   init()
+// })
+const watchScope = watch(
+  () => route.query,
+  () => {
+    init()
+  },
+  {
+    immediate: true
+  }
+)
+onUnmounted(() => {
+  watchScope()
 })
+
+const iframeRef = ref(null)
+const handleIframeLoaded = () => {
+  nextTick(() => {
+    loading.value = false
+  })
+}
 </script>
 
 <template>
-  <div class="system-message-detail">
-    <div class="content">
+  <div class="system-message-detail" v-vloading="loading">
+    <!-- <el-button @click="handleIframeLoaded">测试</el-button> -->
+    <div class="content" v-if="route.query.type !== 'feature'">
       <div class="header" v-if="notificationData.title">
         <div class="status-icon"></div>
         <div class="title">{{ notificationData.title }}</div>
       </div>
-      <div class="total-tips">Latest Status Updates ({{ notificationData.numericRecords }})</div>
+      <div class="total-tips" v-if="notificationData.numericRecords > -1">
+        Latest Status Updates ({{ notificationData.numericRecords }})
+      </div>
+      <div
+        class="total-tips"
+        v-else-if="notificationData.etdOrdeparturNum > -1 || notificationData.etaOrarrivalNum > -1"
+      >
+        <div>
+          <span v-if="notificationData.etdOrdeparturNum"
+            >{{ notificationData.type === 'delay' ? 'Departure Delay' : 'ETD Change' }} ({{
+              notificationData.etdOrdeparturNum
+            }})</span
+          >
+          <span v-if="notificationData.etdOrdeparturNum && notificationData.etaOrarrivalNum">
+            &nbsp;&nbsp;|&nbsp;&nbsp;</span
+          >
+          <span v-if="notificationData.etaOrarrivalNum">
+            {{ notificationData.type === 'delay' ? 'Arrival Delay' : 'ETA Change' }} ({{
+              notificationData.etaOrarrivalNum
+            }})
+          </span>
+        </div>
+      </div>
       <EventCard
         v-for="(item, index) in notificationData.notificationList"
         :key="index"
         :data="item"
       />
     </div>
+    <div class="content" v-else>
+      <div class="header" v-if="notificationData.title">
+        <div class="status-icon"></div>
+        <div class="title">{{ notificationData.title }}</div>
+      </div>
+      <div class="feature-pdf">
+        <iframe
+          ref="iframeRef"
+          :src="iframeUrl"
+          width="100%"
+          height="100%"
+          @load="handleIframeLoaded"
+        ></iframe>
+      </div>
+    </div>
   </div>
 </template>
 
 <style lang="scss" scoped>
 .system-message-detail {
+  height: 100%;
   padding: 16px;
+  overflow: auto;
   .content {
     margin: auto;
-    width: 800px;
+    width: 1000px;
+    height: 100%;
   }
   .notification-card {
     max-width: 800px;
@@ -96,5 +159,8 @@ onMounted(() => {
     line-height: 40px;
     font-size: 12px;
   }
+  .feature-pdf {
+    height: calc(100% - 28px);
+  }
 }
 </style>

+ 12 - 12
src/views/SystemSettings/src/SystemSettings.vue

@@ -2,12 +2,12 @@
 import { ref, onMounted } from 'vue'
 import AddRSettingTableules from './components/SettingTable'
 import MonitoringTable from './components/MonitoringTable'
-import { useRouter } from 'vue-router'
+import { useRouter, useRoute } from 'vue-router'
 import PersonalProfile from './components/PersonalProfile.vue'
 
 const router = useRouter()
-
-const TabActive = ref('Personal Profile')
+const route = useRoute()
+const TabActive = ref(route.query.tab || 'Personal Profile')
 const isMilestoneChecked = ref(false)
 const isContainerChecked = ref(false)
 const isDepartureChecked = ref(false)
@@ -202,19 +202,19 @@ const gettabledatalength = (val: any) => {
   tabledatalength.value = val
 }
 
-// onMounted(() => {
-//   getsubscribe()
-//   if (sessionStorage.getItem('activeTab') != null) {
-//     TabActive.value = sessionStorage.getItem('activeTab')
-//     sessionStorage.removeItem('activeTab')
-//   }
-// })
+onMounted(() => {
+  getsubscribe()
+  if (sessionStorage.getItem('activeTab') != null) {
+    TabActive.value = sessionStorage.getItem('activeTab')
+    sessionStorage.removeItem('activeTab')
+  }
+})
 </script>
 <template>
   <div class="Title">System Settings</div>
   <el-tabs v-model="TabActive" class="demo-tabs">
     <el-tab-pane label="Personal Profile" name="Personal Profile"><PersonalProfile /></el-tab-pane>
-    <!-- <el-tab-pane label="Subscribe Notifications" name="Subscribe Notifications">
+    <el-tab-pane label="Subscribe Notifications" name="Subscribe Notifications">
       <div class="subscribedTitle">Notification Events for Subscribed Shipments</div>
       <div class="SubscribeCollapse">
         <el-collapse v-model="CollapseActive" accordion @change="changeCollapse">
@@ -325,7 +325,7 @@ const gettabledatalength = (val: any) => {
         >
       </div>
       <MonitoringTable @gettabledatalength="gettabledatalength"></MonitoringTable>
-    </el-tab-pane> -->
+    </el-tab-pane>
   </el-tabs>
 </template>
 

+ 2 - 1
src/views/Tracking/src/components/PublicTracking/src/PublicTrackingSearch.vue

@@ -138,7 +138,8 @@ const encryptPassword = (password) => {
     </div>
     <VSliderVerification
       v-if="isShowSliderVerification"
-      @close="confirmVerification"
+      @success="confirmVerification"
+      @close="isShowSliderVerification = false"
       ref="sliderVerificationRef"
     ></VSliderVerification>
   </div>

+ 0 - 222
src/views/Tracking/src/components/PublicTracking/src/components/SlideVerify.vue

@@ -1,222 +0,0 @@
-<script lang="ts" setup>
-const dialogVisible = ref(false)
-
-const openDialog = () => {
-  dialogVisible.value = true
-}
-
-const position = ref(0)
-const isDragging = ref(false)
-const verifyText = ref('Swipe right to verify')
-const sliderState = ref<'start' | 'success' | 'error' | 'dragging'>('start')
-const styleMap = {
-  start: {
-    thumbColor: 'var(--color-neutral-1)',
-    thumbIcon: 'icon-icon_drag__line_b',
-    trackBackground: '#87909e'
-  },
-  dragging: {
-    thumbColor: 'var(--color-neutral-1)',
-    thumbIcon: 'icon-icon_drag__line_b',
-    trackBackground: 'var(--color-success)'
-  },
-  success: {
-    thumbColor: '#fff',
-    thumbIcon: 'icon-icon_confirm_b',
-    trackBackground: 'var(--color-success)'
-  },
-  error: {
-    thumbColor: '#fff',
-    thumbIcon: 'icon-icon_reject_b',
-    trackBackground: '#c7353f'
-  }
-}
-const trackRef = ref<HTMLElement | null>(null)
-
-const getTrackBackground = () => {
-  const trackWidth = trackRef.value?.offsetWidth || 320
-  const progress = (position.value / (trackWidth - 40)) * 100 // 百分比
-  if (sliderState.value === 'start') {
-    return styleMap.start.trackBackground // 初始时灰色
-  } else if (sliderState.value === 'dragging') {
-    return `linear-gradient(90deg, ${styleMap.success.trackBackground} ${progress}%, ${styleMap.start.trackBackground} ${progress}%)`
-  } else if (sliderState.value === 'error') {
-    return `linear-gradient(90deg, ${styleMap.error.trackBackground} ${progress}%, ${styleMap.start.trackBackground} ${progress}%)`
-  }
-  return styleMap.success.trackBackground // 成功时整条绿色
-}
-
-const startDrag = () => {
-  if (sliderState.value === 'success') {
-    return
-  }
-
-  isDragging.value = true
-  document.addEventListener('mousemove', onDrag)
-  document.addEventListener('mouseup', stopDrag)
-}
-
-const onDrag = (event: MouseEvent) => {
-  if (isDragging.value) {
-    if (trackRef.value) {
-      sliderState.value = 'dragging'
-      verifyText.value = 'Swipe right to verify'
-      const rect = trackRef.value.getBoundingClientRect()
-      const offsetX = event.clientX - rect.left
-      position.value = Math.min(Math.max(offsetX, 0), rect.width - 40) // 40是滑块的宽度
-    }
-  }
-}
-
-const emit = defineEmits<{
-  verifySuccess: []
-}>()
-const stopDrag = () => {
-  isDragging.value = false
-  document.removeEventListener('mousemove', onDrag)
-  document.removeEventListener('mouseup', stopDrag)
-
-  if (trackRef.value) {
-    const trackWidth = trackRef.value.offsetWidth
-    if (position.value >= trackWidth - 40) {
-      sliderState.value = 'success'
-      verifyText.value = 'Verification successful'
-      setTimeout(() => {
-        dialogVisible.value = false
-        emit('verifySuccess')
-      }, 500)
-    } else {
-      sliderState.value = 'error'
-      verifyText.value = 'Verification failed'
-      setTimeout(() => {
-        sliderState.value = 'start'
-        verifyText.value = 'Swipe right to verify'
-        position.value = 0
-      }, 3000)
-    }
-  }
-}
-
-const moveSlider = (event: MouseEvent) => {
-  if (sliderState.value !== 'success') {
-    onDrag(event)
-  }
-}
-
-const clearData = () => {
-  sliderState.value = 'start'
-  verifyText.value = 'Swipe right to verify'
-  position.value = 0
-}
-defineExpose({
-  openDialog
-})
-</script>
-
-<template>
-  <el-dialog
-    top="30vh"
-    destroy-on-close
-    :close-on-click-modal="false"
-    :close-on-press-escape="false"
-    @closed="clearData"
-    v-model="dialogVisible"
-    width="400"
-    class="slide-verify-dialog"
-  >
-    <div class="content">
-      <p>Please drag the slider below to complete the</p>
-      <p>verification to ensure normal access</p>
-      <div class="slider-container">
-        <div
-          class="slider-track"
-          :style="{ background: getTrackBackground() }"
-          @click="moveSlider"
-          ref="trackRef"
-        >
-          {{ verifyText }}
-          <div
-            class="slider-thumb"
-            :style="{ left: `${position}px`, borderColor: styleMap[sliderState].trackBackground }"
-            @mousedown="startDrag"
-          >
-            <span
-              v-if="sliderState === 'start' || sliderState === 'dragging'"
-              class="font_family"
-              :style="{ color: styleMap[sliderState].thumbColor }"
-              :class="[styleMap[sliderState].thumbIcon]"
-            ></span>
-            <span
-              v-else
-              class="font_family other-state"
-              :style="{
-                color: styleMap[sliderState].thumbColor,
-                backgroundColor: styleMap[sliderState].trackBackground
-              }"
-              :class="[styleMap[sliderState].thumbIcon]"
-            ></span>
-          </div>
-        </div>
-      </div>
-    </div>
-  </el-dialog>
-</template>
-
-<style lang="scss" scoped>
-.content {
-  padding: 40px 0;
-  text-align: center;
-  & > p {
-    line-height: 21px;
-  }
-}
-
-.slider-container {
-  width: 320px;
-  margin: 16px auto 0;
-  text-align: center;
-  user-select: none;
-}
-.slider-track {
-  position: relative;
-  width: 100%;
-  height: 40px;
-  padding: 1px;
-  background: #868f9d;
-  border-radius: 6px;
-  line-height: 38px;
-  color: #fff;
-}
-.slider-thumb {
-  position: absolute;
-  top: 0px;
-  left: 10px;
-  width: 40px;
-  height: 40px;
-  background: #fff;
-  cursor: pointer;
-  border-radius: 6px;
-  border: 1px solid #868f9d;
-  .font_family {
-    font-size: 14px;
-    &.other-state {
-      height: 16px;
-      width: 16px;
-      padding: 1px;
-      border-radius: 50%;
-      font-size: 14px;
-    }
-  }
-}
-</style>
-
-<style lang="scss">
-.slide-verify-dialog {
-  .el-dialog__header {
-    display: none;
-  }
-  .el-dialog__body {
-    padding: 0;
-  }
-}
-</style>

+ 11 - 3
src/views/Tracking/src/components/TrackingDetail/src/TrackingDetail.vue

@@ -109,7 +109,15 @@ const getData = () => {
       loading.value = false
     })
 }
-getData()
+watch(
+  () => route.query,
+  () => {
+    getData()
+  },
+  {
+    immediate: true
+  }
+)
 
 const originRef = ref()
 const destinationRef = ref()
@@ -167,7 +175,7 @@ const SubscribeShipments = () => {
             >AMS/ISF</el-button
           >
 
-          <!-- <el-button
+          <el-button
             class="recent_button"
             @click="SubscribeShipments"
             :class="is_subscribe ? 'IsSubscribe' : ''"
@@ -183,7 +191,7 @@ const SubscribeShipments = () => {
               </svg>
             </span>
             <span class="Subscribe">Subscribe</span>
-          </el-button> -->
+          </el-button>
         </div>
       </div>
       <div class="detail-info">

+ 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,保留两位小数
 }
 

+ 2 - 2
src/views/Tracking/src/components/TrackingTable/src/TrackingTable.vue

@@ -628,7 +628,7 @@ defineExpose({
           <span style="font-size: 12px">VGM</span>
         </el-button>
 
-        <!-- <el-button
+        <el-button
           class="recent_button el-button--blue"
           @click="SubscribeShipments(row)"
           :class="row.is_subscribe ? 'IsSubscribe' : ''"
@@ -643,7 +643,7 @@ defineExpose({
               <use xlink:href="#icon-icon_unmark_b"></use>
             </svg>
           </span>
-        </el-button> -->
+        </el-button>
       </template>
       <!-- Transportation Mode字段的插槽 -->
       <template #mode="{ row, column }">