Переглянути джерело

Merge branch 'dev_zyh' of United_Software/k_online_ui into feature

Jack Zhou 9 місяців тому
батько
коміт
66876d3552
53 змінених файлів з 2101 додано та 686 видалено
  1. 2 0
      src/api/index.ts
  2. 20 0
      src/api/module/notificationMessage.ts
  3. 2 1
      src/auto-imports.d.ts
  4. 1 0
      src/components/NotificationMessageCard/index.ts
  5. 21 0
      src/components/NotificationMessageCard/src/NotificationMessageCard.vue
  6. 331 0
      src/components/NotificationMessageCard/src/components/EventCard.vue
  7. 101 0
      src/components/NotificationMessageCard/src/components/FeatureUpdateCard.vue
  8. 87 0
      src/components/NotificationMessageCard/src/components/PasswordCard.vue
  9. BIN
      src/components/NotificationMessageCard/src/images/icon_publish.png
  10. BIN
      src/components/NotificationMessageCard/src/images/test.png
  11. 4 1
      src/components/ScoringGrade/src/ScoringGrade.vue
  12. 2 6
      src/components/ShipmentStatus/src/ShipmentStatus.vue
  13. 16 3
      src/router/index.ts
  14. 22 2
      src/stores/modules/breadCrumb.ts
  15. 27 8
      src/stores/modules/user.ts
  16. 7 7
      src/styles/elementui.scss
  17. 16 4
      src/styles/icons/iconfont.css
  18. 0 0
      src/styles/icons/iconfont.js
  19. 6 0
      src/styles/icons/iconfont.svg
  20. BIN
      src/styles/icons/iconfont.ttf
  21. BIN
      src/styles/icons/iconfont.woff
  22. BIN
      src/styles/icons/iconfont.woff2
  23. 1 0
      src/styles/theme.scss
  24. 0 6
      src/utils/axios.ts
  25. 76 8
      src/utils/tools.ts
  26. 1 1
      src/views/Booking/src/components/BookingDetail/src/BookingDetail.vue
  27. 12 3
      src/views/Booking/src/components/BookingDetail/src/components/BasicInformation.vue
  28. 6 11
      src/views/Booking/src/components/BookingDetail/src/components/ContainersView.vue
  29. 3 3
      src/views/Booking/src/components/BookingDetail/src/components/EmailView.vue
  30. 6 7
      src/views/Booking/src/components/BookingTable/src/BookingTable.vue
  31. 5 1
      src/views/Dashboard/src/components/ScoringSystem.vue
  32. 48 13
      src/views/Layout/src/components/Header/HeaderView.vue
  33. 268 0
      src/views/Layout/src/components/Header/components/NotificationDrawer.vue
  34. 199 0
      src/views/Layout/src/components/Header/components/TrainingCard.vue
  35. 45 7
      src/views/Layout/src/components/Menu/MenuView.vue
  36. 1 4
      src/views/Login/src/components/ChangePasswordCard.vue
  37. 4 3
      src/views/Login/src/loginView.vue
  38. 4 10
      src/views/OperationLog/src/components/BookingTable/src/BookingTable.vue
  39. 0 510
      src/views/OperationLog/src/components/BookingTable/src/BookingTableColumns.ts
  40. 1 0
      src/views/SystemMessage/index.ts
  41. 259 0
      src/views/SystemMessage/src/SystemMessage.vue
  42. 348 0
      src/views/SystemMessage/src/components/PersonalProfile.vue
  43. 86 0
      src/views/SystemMessage/src/components/SystemMessageDetail.vue
  44. 13 3
      src/views/Tracking/src/components/PublicTracking/src/components/BasicInformation.vue
  45. 1 1
      src/views/Tracking/src/components/PublicTracking/src/components/PublicTrackingDetail.vue
  46. 1 6
      src/views/Tracking/src/components/TrackingDetail/src/TrackingDetail.vue
  47. 14 5
      src/views/Tracking/src/components/TrackingDetail/src/components/BasicInformation.vue
  48. 6 11
      src/views/Tracking/src/components/TrackingDetail/src/components/ContainersView.vue
  49. 2 2
      src/views/Tracking/src/components/TrackingDetail/src/components/EmailDrawer.vue
  50. 1 1
      src/views/Tracking/src/components/TrackingDetail/src/components/MapView.vue
  51. 8 18
      src/views/Tracking/src/components/TrackingDetail/src/components/RoutesView.vue
  52. 6 7
      src/views/Tracking/src/components/TrackingTable/src/TrackingTable.vue
  53. 11 13
      src/views/Tracking/src/components/TrackingTable/src/components/VGMView.vue

+ 2 - 0
src/api/index.ts

@@ -3,6 +3,7 @@ import * as tracking from './module/tracking'
 import * as common from './module/common'
 import * as login from './module/login'
 import * as other from './module/other'
+import * as notificationMessage from './module/notificationMessage'
 import * as system from './module/system'
 /**
  * api 对象接口定义
@@ -21,6 +22,7 @@ const apis = generateApiMap({
   ...common,
   ...login,
   ...other,
+  ...notificationMessage,
   ...system
 })
 export default {

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

@@ -0,0 +1,20 @@
+import HttpAxios from '@/utils/axios'
+
+const base = import.meta.env.VITE_API_HOST
+const baseUrl = `${base}/main_new_version.php`
+
+/**
+ * 保存用户个人信息或日期和数字格式化配置
+ * @param save_model profile 代表基本信息的save, no_profile 代表格式信息的save
+ * @param user_name
+ * @param email
+ * @param date_fromat
+ * @param numbers_format
+ */
+export const saveUserInfo = (params: any) => {
+  return HttpAxios.get(`${baseUrl}`, {
+    action: 'system_setting',
+    operate: 'personal_profile_save',
+    ...params
+  })
+}

+ 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 - 0
src/components/NotificationMessageCard/index.ts

@@ -0,0 +1 @@
+export { default } from './src/NotificationMessageCard.vue'

+ 21 - 0
src/components/NotificationMessageCard/src/NotificationMessageCard.vue

@@ -0,0 +1,21 @@
+<script setup lang="ts">
+import EventCard from './components/EventCard.vue'
+import PasswordCard from './components/PasswordCard.vue'
+import FeatureUpdateCard from './components/FeatureUpdateCard.vue'
+
+const props = defineProps<{
+  data: any
+}>()
+</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>
+</template>
+
+<style lang="scss" scoped></style>

+ 331 - 0
src/components/NotificationMessageCard/src/components/EventCard.vue

@@ -0,0 +1,331 @@
+<script setup lang="ts">
+import { transportationMode } from '@/components/transportationMode'
+import { useRouter } from 'vue-router'
+import { getTimezone } from '@/utils/tools'
+
+const router = useRouter()
+
+interface EventCardPropsData {
+  type: string // 'milestone' | 'container' | 'delay' | 'change'
+  numericRecords?: number // 多条记录数 (Daily Update消息)
+  isRead: boolean // 是否已读 (true 已读,false 未读)
+  title?: string // Milestone Update
+  mode?: string // 运输方式
+  no: string // HBOL: SHJN2301234
+  tag: string // tag  Booking Confirmed
+  location: string
+  timezone?: string // 时区
+  time: string
+  timeLabel: string
+  previous?: Array<string>
+  info?: {
+    route?: []
+    etdOrdeparturNum?: number
+    etaOrarrivalNum?: number
+    time: string
+    timeLabel: string
+    delayTimeTip: string
+    timezone?: string // 时区
+    leg?: []
+  }
+}
+
+const props = defineProps<{
+  data: EventCardPropsData
+}>()
+
+const handleSeeAll = (data: any) => {
+  router.push({
+    name: 'System Message Detail',
+    params: {
+      data: JSON.stringify(data) // 将数据转换为字符串并作为查询参数传递
+    }
+  })
+}
+</script>
+
+<template>
+  <div class="notification-card" :class="{ 'is-read': data.isRead }">
+    <div class="header" v-if="data.title">
+      <div class="status-icon"></div>
+      <div class="title">{{ data.title }}</div>
+    </div>
+    <div class="content">
+      <div
+        class="more-tips"
+        v-if="(data.type === 'milestone' || 'container') && data.numericRecords"
+      >
+        <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>
+          <span v-if="data.info.etdOrdeparturNum"
+            >Departure Delay ({{ data.info.etdOrdeparturNum }})</span
+          >
+          <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>
+        </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="base-info">
+        <!-- 除了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>
+        </div>
+        <!-- container类型显示图标 -->
+        <div v-else>
+          <span class="font_family icon-icon_container__filled_b"></span>
+          <span class="no">Container: {{ data.no }}</span>
+        </div>
+        <div class="tag" :class="{ delay: data.type === 'delay', change: data.type === 'change' }">
+          <span class="dot"></span>
+          <span class="text">{{ data.tag }}</span>
+        </div>
+      </div>
+      <div class="route" v-if="data?.info?.route?.length > 0">
+        <span class="font_family icon-icon_route_b"></span>
+        <span>Route:&nbsp;</span>
+        <template v-for="(item, index) in data.info.route" :key="index">
+          <span>{{ item }}</span
+          ><span style="margin: 0 3px" v-if="index !== data.info.route.length - 1">→</span>
+        </template>
+      </div>
+      <!-- change多程情况中的Leg-->
+      <div class="location" v-if="data?.info?.leg?.length > 0">
+        <span class="font_family icon-icon_location_b"></span>
+        <span>Current Leg:&nbsp;</span>
+        <template v-for="(item, index) in data.info.leg" :key="index">
+          <span>{{ item }}</span
+          ><span style="margin: 0 3px" v-if="index !== data.info.leg.length - 1">→</span>
+        </template>
+      </div>
+      <div class="location" v-if="data.location">
+        <span class="font_family icon-icon_location_b"></span>
+        <span>{{ data.location }}</span>
+      </div>
+      <div class="delay-time" v-if="data.type === 'delay' && data.info?.time">
+        <span class="font_family icon-icon_delay_b"></span>
+        <span>{{ data.info.timeLabel }}</span>
+        <span>{{ data.info.time }}</span>
+        <span>{{ getTimezone(data.info.timezone) }}</span>
+      </div>
+      <div class="change-time" v-if="data.type === 'change' && data.info?.time">
+        <span style="margin-left: 1px" class="font_family icon-icon_time_b"></span>
+        <span>{{ data.info?.time }}</span>
+      </div>
+      <div class="time">
+        <span style="margin-left: 1px" class="font_family icon-icon_time_b"></span>
+        <span>{{ data.time }}</span>
+      </div>
+      <div class="previous" v-if="data.previous">
+        <span class="previous-icon"></span>
+        <span>{{ data.previous }}</span>
+      </div>
+    </div>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.notification-card {
+  // max-width: 640px;
+  margin-bottom: 16px;
+  &.is-read {
+    & > .header {
+      .status-icon {
+        background-color: var(--color-border);
+      }
+      .title {
+        color: #b5b9bf;
+      }
+    }
+  }
+  .header {
+    display: flex;
+    align-items: center;
+    margin-bottom: 8px;
+    .status-icon {
+      width: 8px;
+      height: 8px;
+      border-radius: 50%;
+      background-color: var(--color-theme);
+      margin-right: 10px;
+    }
+    .title {
+      font-weight: 700;
+    }
+  }
+  .content {
+    padding: 16px 8px;
+    padding-top: 0;
+    background-color: var(--color-header-bg);
+    border-radius: 6px;
+    .more-tips {
+      display: flex;
+      justify-content: space-between;
+      padding: 8px 0;
+      border-bottom: 1px dashed var(--color-border);
+      font-size: 12px;
+      line-height: 24px;
+      .see-all-icon {
+        width: 68px;
+        height: 24px;
+        :deep(span) {
+          color: var(--color-theme);
+        }
+      }
+    }
+    .base-info {
+      display: flex;
+      padding-top: 16px;
+      .no {
+        margin-left: 8px;
+        font-weight: 700;
+        line-height: 18px;
+      }
+      .tag {
+        display: flex;
+        align-items: center;
+        margin-left: 4px;
+        padding-left: 8px;
+        padding-right: 6px;
+        background-color: #e6f1eb;
+        border-radius: 3px;
+        &.delay {
+          background-color: #f7e7e9;
+          .dot {
+            background-color: #c9353f;
+          }
+          .text {
+            color: #c9353f;
+          }
+        }
+        &.change {
+          background-color: #f5f2e6;
+          .dot {
+            background-color: #edb82f;
+          }
+          .text {
+            margin-top: 0;
+            color: #edb82f;
+          }
+        }
+        .dot {
+          width: 5px;
+          height: 5px;
+          border-radius: 50%;
+          background-color: #5bb462;
+          margin-right: 4px;
+        }
+        .text {
+          margin-top: 2px;
+          font-size: 10px;
+          font-weight: 600;
+          color: #5bb462;
+          line-height: 10px;
+          vertical-align: middle;
+        }
+      }
+    }
+    .route,
+    .location,
+    .change-time,
+    .delay-time,
+    .time {
+      display: flex;
+      align-items: center;
+      margin-top: 8px;
+      height: 16px;
+      span {
+        color: var(--color-neutral-2);
+        font-size: 12px;
+        line-height: 16px;
+      }
+      .font_family {
+        font-size: 16px;
+        font-family: 'iconfont';
+        color: var(--color-neutral-2);
+        margin-right: 8px;
+      }
+    }
+    div.delay-time {
+      margin-top: 6px;
+      span,
+      .font_family {
+        color: #c9353f;
+      }
+      span {
+        line-height: 19px;
+      }
+      .font_family {
+        line-height: 12px;
+        margin-left: -1px;
+        margin-right: 6px;
+        font-size: 19px;
+      }
+    }
+    div.change-time {
+      span,
+      .font_family {
+        color: #edb82f;
+      }
+    }
+    .previous {
+      height: 24px;
+      margin-top: 8px;
+      padding-left: 8px;
+      line-height: 24px;
+      background-color: #e1e3e9;
+      border-radius: 6px;
+      span {
+        font-size: 12px;
+      }
+      .previous-icon {
+        display: inline-block;
+        width: 4px;
+        height: 4px;
+        background-color: var(--color-neutral-1);
+        border-radius: 50%;
+        margin-right: 8px;
+        vertical-align: middle;
+      }
+    }
+  }
+}
+</style>

+ 101 - 0
src/components/NotificationMessageCard/src/components/FeatureUpdateCard.vue

@@ -0,0 +1,101 @@
+<script setup lang="ts">
+interface FeatureUpdateCardPropsData {
+  title: string
+  header: string
+  content: string
+  isRead: boolean
+  imgSrc: string
+}
+
+const props = defineProps<{
+  data: FeatureUpdateCardPropsData
+}>()
+</script>
+
+<template>
+  <div>
+    <div class="feature-update" :class="{ 'is-read': data.isRead }">
+      <div class="header">
+        <div class="status-icon"></div>
+        <div class="title">{{ data.title }}</div>
+      </div>
+      <div class="card-content">
+        <div class="title">
+          <!-- <span class="font_family icon-icon_password_b"></span> -->
+          <img style="margin-right: 5px" src="../images/icon_publish.png" alt="" />
+          <span class="gradient-text">{{ data.header }}</span>
+        </div>
+        <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="change-btn" style="text-align: center">
+          <el-button class="el-button--main" style="height: 40px; padding: 0 32px">
+            View more</el-button
+          >
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.feature-update {
+  margin-bottom: 16px;
+  &.is-read {
+    & > .header {
+      .status-icon {
+        background-color: var(--color-border);
+      }
+      .title {
+        color: #b5b9bf;
+      }
+    }
+  }
+  .header {
+    display: flex;
+    align-items: center;
+    margin-bottom: 8px;
+    .status-icon {
+      width: 8px;
+      height: 8px;
+      border-radius: 50%;
+      background-color: var(--color-theme);
+      margin-right: 10px;
+    }
+    .title {
+      font-weight: 700;
+    }
+  }
+  .card-content {
+    padding: 16px 16px 24px 8px;
+    background: linear-gradient(137deg, #fff4eb 12.41%, #f0f3ff 52.63%, #e0f7f9 93.28%);
+    border-radius: 12px;
+    .title {
+      img {
+        vertical-align: middle;
+      }
+      span {
+        vertical-align: middle;
+        display: inline-block;
+        font-weight: 700;
+      }
+      .gradient-text {
+        background: linear-gradient(90deg, #dc6c6d 0%, #7959c8 46%, #ed6d00 100%);
+        background-clip: text;
+        -webkit-background-clip: text;
+        color: transparent;
+        font-size: 14px;
+        font-weight: bold;
+      }
+      .font_family {
+        margin-right: 8px;
+      }
+    }
+    .content-text {
+      margin: 8px 0 0px 24px;
+    }
+  }
+}
+</style>

+ 87 - 0
src/components/NotificationMessageCard/src/components/PasswordCard.vue

@@ -0,0 +1,87 @@
+<script setup lang="ts">
+interface PasswordCardPropsData {
+  title: string
+  isExpiration: boolean
+  tips: string
+  isRead: boolean
+  content: string
+}
+const props = defineProps<{
+  data: PasswordCardPropsData
+}>()
+</script>
+
+<template>
+  <div class="password-notifications" :class="{ 'is-read': data.isRead }">
+    <div class="header" v-if="data.title">
+      <div class="status-icon"></div>
+      <div class="title">{{ data.title }}</div>
+    </div>
+    <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>
+      </div>
+      <div class="details">
+        {{ data.content }}
+      </div>
+      <div class="change-btn" style="text-align: center">
+        <el-button 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>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.password-notifications {
+  margin-bottom: 16px;
+  &.is-read {
+    & > .header {
+      .status-icon {
+        background-color: var(--color-border);
+      }
+      .title {
+        color: #b5b9bf;
+      }
+    }
+  }
+  .header {
+    display: flex;
+    align-items: center;
+    margin-bottom: 8px;
+    .status-icon {
+      width: 8px;
+      height: 8px;
+      border-radius: 50%;
+      background-color: var(--color-theme);
+      margin-right: 10px;
+    }
+    .title {
+      font-weight: 700;
+    }
+  }
+  .card-content {
+    padding: 16px 8px 24px;
+    background: linear-gradient(180deg, #ffe294 0%, #f6f8fa 100%);
+    border-radius: 12px;
+    &.is-expired {
+      background: linear-gradient(182deg, #ef99a0 2.2%, #f6f8fa 98.77%);
+    }
+    .title {
+      span {
+        vertical-align: middle;
+        font-weight: 700;
+      }
+      .font_family {
+        margin-right: 8px;
+      }
+    }
+    .details {
+      margin: 8px 0 16px 24px;
+    }
+  }
+}
+</style>

BIN
src/components/NotificationMessageCard/src/images/icon_publish.png


BIN
src/components/NotificationMessageCard/src/images/test.png


+ 4 - 1
src/components/ScoringGrade/src/ScoringGrade.vue

@@ -14,6 +14,9 @@ import happyPng from '../image/score_happy.png'
 import happyPng2 from '../image/happy_2.png'
 import normalPng from '../image/score_normal.png'
 import submitsucessful from '../image/submit_successful.png'
+import { useUserStore } from '@/stores/modules/user'
+
+const userStore = useUserStore()
 
 // const isShow = ref(true)
 const visible = ref(false)
@@ -202,7 +205,7 @@ const changeSmileRadio = (val: any) => {
 }
 // 提交details
 const submitDetails = (val: any) => {
-  const username = localStorage.getItem('account') ? localStorage.getItem('account') : ''
+  const username = userStore.userName
   if (angryCheckbox.value.length) {
     $api
       .scoringgrade({

+ 2 - 6
src/components/ShipmentStatus/src/ShipmentStatus.vue

@@ -1,6 +1,6 @@
 <script setup lang="ts">
-import dayjs from 'dayjs'
 import { useOverflow } from '@/hooks/useOverflow'
+import { formatTimezone } from '@/utils/tools'
 
 const props = defineProps({
   data: Object
@@ -34,10 +34,6 @@ const getDateHeight = (index: number) => {
   return 42 + 26 * index
 }
 
-const formatDate = (date: string) => {
-  return date ? dayjs(date).format('MMM-DD-YYYY') : '--'
-}
-
 const pathRef = ref()
 
 const { isOverflow: isPathOverflow } = useOverflow(pathRef, props)
@@ -76,7 +72,7 @@ const { isOverflow: isPathOverflow } = useOverflow(pathRef, props)
             <div class="step-dot"></div>
             <div class="label">{{ dateItem.label }}</div>
             <div class="divider"></div>
-            <div class="date">{{ formatDate(dateItem.date) }}</div>
+            <div class="date">{{ formatTimezone(dateItem.date) }}</div>
           </div>
         </div>
       </div>

+ 16 - 3
src/router/index.ts

@@ -88,6 +88,19 @@ const router = createRouter({
           name: 'Operationlog',
           component: () => import('../views/OperationLog')
         },
+        {
+          path: '/system-message',
+          name: 'System Message',
+          component: () => import('../views/SystemMessage')
+        },
+        {
+          path: '/system-message-detail',
+          name: 'System Message Detail',
+          meta: {
+            breadName: 'Detail'
+          },
+          component: () => import('../views/SystemMessage/src/components/SystemMessageDetail.vue')
+        },
         {
           path: '/SystemSettings',
           name: 'Monitoring Settings',
@@ -109,10 +122,10 @@ const router = createRouter({
 // * 路由拦截 beforeEach
 router.beforeEach(async (to, from, next) => {
   useBreadCrumb().setRouteList(to)
+  const userStore = useUserStore()
   // 如果手动跳转登录页,清除登录信息
   if (to.path === '/login') {
-    if (localStorage.getItem('username')) {
-      const userStore = useUserStore()
+    if (userStore.userInfo?.uname) {
       await userStore.logout()
     }
     sessionStorage.removeItem('trackingTablePageInfo')
@@ -122,7 +135,7 @@ router.beforeEach(async (to, from, next) => {
   // 未登录白名单
   const whiteList = ['/login', '/public-tracking', '/public-tracking/detail', '/reset-password']
   // 判断是否登录
-  if (!whiteList.includes(to.path) && !localStorage.getItem('username')) {
+  if (!whiteList.includes(to.path) && !userStore.userInfo?.uname) {
     const userStore = useUserStore()
     await userStore.logout()
     if (whiteList.includes(from.path)) {

+ 22 - 2
src/stores/modules/breadCrumb.ts

@@ -9,7 +9,14 @@ interface BreadCrumb {
   routeList: Route[]
 }
 // 需要添加多级菜单的页面,值为route的name
-const whiteList = ['Booking Detail', 'Tracking Detail', 'Add VGM', 'Public Tracking Detail','Create New Rule']
+const whiteList = [
+  'Booking Detail',
+  'Tracking Detail',
+  'Add VGM',
+  'Public Tracking Detail',
+  'Create New Rule',
+  'System Message Detail'
+]
 
 export const useBreadCrumb = defineStore('breadCrumb', {
   state: (): BreadCrumb => ({
@@ -34,9 +41,22 @@ export const useBreadCrumb = defineStore('breadCrumb', {
             query: toRoute.query
           }
         ]
+      } else if (toRoute.name === 'System Message Detail') {
+        this.routeList = [
+          {
+            label: 'System Message',
+            path: '/system-message',
+            query: ''
+          },
+          {
+            label: 'System Message Detail',
+            path: '/system-message/detail',
+            query: toRoute.query
+          }
+        ]
       } else if (toRoute.name && whiteList.includes(toRoute.name)) {
         this.routeList.push({
-          label: toRoute.name,
+          label: toRoute?.meta?.breadName || toRoute.name,
           path: toRoute.path,
           query: toRoute.query
         })

+ 27 - 8
src/stores/modules/user.ts

@@ -1,33 +1,52 @@
 import { defineStore } from 'pinia'
 import { useVisitedRowState } from './visitedRow'
 
+interface UserInfo {
+  uname: string
+  first_name: string
+  last_name: string
+  user_type: string
+  email: string
+  expire_day: number
+  date_format: string
+  numbers_format: string
+}
 interface UserState {
-  username: string
+  userInfo: UserInfo
   isFirstLogin: boolean
 }
 export const useUserStore = defineStore('user', {
   state: (): UserState => ({
-    username: localStorage.getItem('username') || '',
+    userInfo: JSON.parse(localStorage.getItem('userInfo') || '{}'),
     isFirstLogin: localStorage.getItem('isFirstLogin')
       ? JSON.parse(localStorage.getItem('isFirstLogin'))
       : false
   }),
-  getters: {},
+  getters: {
+    userName(state) {
+      if (state.userInfo.first_name && state.userInfo.last_name) {
+        return `${state.userInfo.first_name} ${state.userInfo.last_name}`
+      } else {
+        return state.userInfo.uname || ''
+      }
+    }
+  },
   actions: {
-    setUsername(username: any, isFirstLogin?: boolean) {
-      localStorage.setItem('username', username)
-      this.username = username
+    setUserInfo(userInfo: any, isFirstLogin?: boolean) {
+      localStorage.setItem('userInfo', JSON.stringify(userInfo))
+      this.userInfo = userInfo
       if (isFirstLogin !== undefined) {
         localStorage.setItem('isFirstLogin', JSON.stringify(isFirstLogin))
         this.isFirstLogin = isFirstLogin
       }
     },
+
     async logout(isNeedLogout: boolean = true) {
       if (isNeedLogout) {
         await $api.logout().then(() => {})
       }
-      localStorage.removeItem('username')
-      this.username = ''
+      localStorage.removeItem('userInfo')
+      this.userInfo = {}
       if (localStorage.getItem('isFirstLogin')) {
         localStorage.removeItem('isFirstLogin')
       }

+ 7 - 7
src/styles/elementui.scss

@@ -49,7 +49,7 @@ button.el-button.el-button--text {
 
 .el-button--main.is-plain {
   background-color: var(--color-white);
-  border: 1px solid var(--color-border);
+  border: 1px solid var(--color-theme);
   span {
     color: var(--color-theme);
   }
@@ -274,6 +274,7 @@ label.el-radio {
     background-color: var(--color-theme);
     border-color: var(--color-theme);
   }
+
   .el-radio__inner {
     height: 16px;
     width: 16px;
@@ -456,11 +457,11 @@ html.dark .el-checkbox.el-checkbox--large span.el-checkbox__inner::after {
   width: 5px; /* 打勾图标宽度 */
   height: 10px; /* 打勾图标高度 */
 }
-div .el-popper__arrow,
-div .el-popper__arrow:before {
-  // height: 0;
-  // width: 0;
-}
+// div .el-popper__arrow,
+// div .el-popper__arrow:before {
+// height: 0;
+// width: 0;
+// }
 .el-popper.is-dark,
 div.el-popper.is-dark > .el-popper__arrow:before {
   background-color: var(--color-el-popper-bg);
@@ -531,7 +532,6 @@ div .el-select-dropdown__item.is-hovering {
 .el-select-dropdown__item {
   border-radius: var(--border-radius-6);
   margin: 0 8px;
-
   margin-bottom: 4px;
   &:last-child {
     margin-bottom: 0;

+ 16 - 4
src/styles/icons/iconfont.css

@@ -1,9 +1,9 @@
 @font-face {
   font-family: "font_family"; /* Project id 4672385 */
-  src: url('iconfont.woff2?t=1737535437731') format('woff2'),
-       url('iconfont.woff?t=1737535437731') format('woff'),
-       url('iconfont.ttf?t=1737535437731') format('truetype'),
-       url('iconfont.svg?t=1737535437731#font_family') format('svg');
+  src: url('iconfont.woff2?t=1740548496100') format('woff2'),
+       url('iconfont.woff?t=1740548496100') format('woff'),
+       url('iconfont.ttf?t=1740548496100') format('truetype'),
+       url('iconfont.svg?t=1740548496100#font_family') format('svg');
 }
 
 .font_family {
@@ -14,6 +14,18 @@
   -moz-osx-font-smoothing: grayscale;
 }
 
+.icon-icon_collapse_b:before {
+  content: "\e707";
+}
+
+.icon-icon_cancel_b1:before {
+  content: "\e706";
+}
+
+.icon-icon_container__maintenance_b:before {
+  content: "\e705";
+}
+
 .icon-icon_up_b:before {
   content: "\e704";
 }

Різницю між файлами не показано, бо вона завелика
+ 0 - 0
src/styles/icons/iconfont.js


+ 6 - 0
src/styles/icons/iconfont.svg

@@ -14,6 +14,12 @@
     />
       <missing-glyph />
       
+      <glyph glyph-name="icon_collapse_b" unicode="&#59143;" d="M199.111111 726.300444a91.022222 91.022222 0 0 1-91.022222-91.022222v-502.556444a91.022222 91.022222 0 0 1 91.022222-91.022222h625.777778a91.022222 91.022222 0 0 1 91.022222 91.022222V635.278222a91.022222 91.022222 0 0 1-91.022222 91.022222h-625.777778z m-22.755555-91.022222c0 12.515556 10.24 22.755556 22.755555 22.755556h217.144889v-548.067556H199.111111a22.755556 22.755556 0 0 0-22.755555 22.755556V635.278222z m308.167111-525.312V658.033778H824.888889c12.515556 0 22.755556-10.24 22.755555-22.755556v-502.556444a22.755556 22.755556 0 0 0-22.755555-22.755556H484.522667z"  horiz-adv-x="1024" />
+      
+      <glyph glyph-name="icon_cancel_b1" unicode="&#59142;" d="M235.690667 704c0 12.515556 10.24 22.755556 22.755555 22.755556h492.942222c12.515556 0 22.755556-10.24 22.755556-22.755556v-407.324444h68.266667v407.324444a91.022222 91.022222 0 0 1-91.022223 91.022222H258.446222a91.022222 91.022222 0 0 1-91.022222-91.022222v-625.777778a91.022222 91.022222 0 0 1 91.022222-91.022222h246.499556v68.266667H258.446222a22.755556 22.755556 0 0 0-22.755555 22.755555v625.777778z m380.643555-442.424889l95.971556-96.028444 95.971555 95.971555 48.241778-48.241778-95.914667-95.971555 95.971556-95.971556-48.298667-48.241777-95.971555 95.914666-95.971556-95.971555-48.298666 48.298666 96.028444 95.971556-96.028444 95.971555 48.298666 48.298667z"  horiz-adv-x="1024" />
+      
+      <glyph glyph-name="icon_container__maintenance_b" unicode="&#59141;" d="M416.448 697.664a32 32 0 0 1-6.848-0.704l-302.4-59.584a32 32 0 0 1-25.856-31.36v-445.76a32 32 0 0 1 25.856-31.424l302.4-59.584a32 32 0 0 1 10.56-0.512l228.096 22.976-6.4 63.68-193.408-19.52V630.272l528.896-53.248v-107.776h64V605.952a32 32 0 0 1-28.8 31.872l-592.384 59.648a32.128 32.128 0 0 1-3.712 0.192zM283.712 606.912l100.736 19.84v-487.296l-100.736 19.84V358.144h31.872v51.2h-31.872V606.912z m-51.2-437.568l-87.168 17.216V579.648l87.168 17.152v-187.456H198.4v-51.2h34.112v-188.8z m517.312 247.488a28.8 28.8 0 0 0 24.96 14.4h167.68c10.24 0 19.84-5.44 24.96-14.4l83.84-145.216c5.12-8.96 5.12-19.84 0-28.8l-83.84-145.216a28.8 28.8 0 0 0-24.96-14.4h-167.68a28.8 28.8 0 0 0-24.96 14.4l-83.84 145.28a28.8 28.8 0 0 0 0 28.8l83.84 145.152z m41.6-43.2l-67.2-116.416 67.2-116.416h134.4l67.2 116.48-67.2 116.352h-134.4z m67.2-79.104a37.312 37.312 0 1 1 0-74.688 37.312 37.312 0 0 1 0 74.688z m-94.912-37.376a94.976 94.976 0 1 0 189.888 0 94.976 94.976 0 0 0-189.888 0z"  horiz-adv-x="1088" />
+      
       <glyph glyph-name="icon_up_b" unicode="&#59140;" d="M503.488 519.104a32 32 0 0 0 44.992 0l227.072-224.64a32 32 0 0 0-22.528-54.784H299.008A32 32 0 0 0 276.48 294.4l227.008 224.704z"  horiz-adv-x="1088" />
       
       <glyph glyph-name="icon_route_b" unicode="&#59136;" d="M428.544 674.816a131.008 131.008 0 1 0-0.768-76.8H332.672a89.024 89.024 0 0 1 0-178.112h387.2a165.312 165.312 0 0 0 0-330.56h-43.072a130.944 130.944 0 1 0 3.2 76.8h39.872a88.512 88.512 0 1 1 0 176.96h-387.2a165.824 165.824 0 1 0 0 331.712h95.872z"  horiz-adv-x="1088" />

BIN
src/styles/icons/iconfont.ttf


BIN
src/styles/icons/iconfont.woff


BIN
src/styles/icons/iconfont.woff2


+ 1 - 0
src/styles/theme.scss

@@ -251,6 +251,7 @@
   --color-dot-unchecked: #eeeeee;
   --color-dot-checked: #ed6d00;
 
+  --color-system-message-nav-bg: #f6f6f6;
   --color-upload-file-bg: #fef8f2;
   --color-upload-file-color: #b5b9bf;
   --color-upload-file-border-bg: #f5b279;

+ 0 - 6
src/utils/axios.ts

@@ -2,10 +2,6 @@ import axios, { type AxiosInstance, type AxiosRequestConfig, type AxiosResponse
 import router from '@/router'
 import { ElMessage, ElMessageBox } from 'element-plus'
 import { useUserStore } from '@/stores/modules/user'
-// import {
-//   showFullScreenLoading,
-//   tryHideFullScreenLoading
-// } from '@monorepo/shared/utils/serviceLoading'
 
 interface codeMessage {
   [key: number]: string
@@ -156,5 +152,3 @@ class HttpAxios {
 }
 
 export default new HttpAxios({})
-
-// export default new HttpAxios({})

+ 76 - 8
src/utils/tools.ts

@@ -1,24 +1,36 @@
 import moment from 'moment-timezone'
 
-export const formatTimezone = (time: string, timezone: string) => {
+const formatString = 'MMM/DD/YYYY'
+export const formatTimezone = (time: string, timezone?: string) => {
   if (!time) return '--'
   let formattedTime = ''
   if (time.length > 12) {
-    formattedTime = moment(time).format('MMM-DD-YYYY hh:mm A')
+    formattedTime = moment(time).format(`${formatString} hh:mm A`)
     if (!timezone) {
       return formattedTime
     }
-    let gmtOffset = ''
-    const timeZoneOffset = moment().tz(timezone).format('Z')
-    // 替换 "+07:00" 为 "GMT+07"
-    gmtOffset = `(GMT${timeZoneOffset.slice(0, 3)})`
-    return `${formattedTime} ${gmtOffset}`
+    let utcOffset = ''
+    const timeZoneOffset = moment.tz(`${moment().year()}-01-01`, timezone).format('Z')
+    // 替换 "+07:00" 为 "UTC+07"
+    utcOffset = `(UTC${timeZoneOffset.slice(0, 3)})`
+    return `${formattedTime} ${utcOffset}`
   } else {
-    formattedTime = moment(time).format('MMM-DD-YYYY')
+    formattedTime = moment(time).format(formatString)
     return formattedTime
   }
 }
 
+/**
+ * 返回传入地区的UTC时区格式化
+ * @param timezone
+ * @returns
+ */
+export const getTimezone = (timezone: string): string => {
+  if (timezone) return ''
+  const offset = moment.tz(`${moment().year()}-01-01`, timezone).format('Z')
+  return `UTC${offset.slice(0, 3)}`
+}
+
 export const formatTimezoneByUTCorGMT = (time: string, timezone: string) => {
   if (!time) return '--'
   let formattedTime = ''
@@ -36,3 +48,59 @@ export const formatTimezoneByUTCorGMT = (time: string, timezone: string) => {
     return `${formattedTime} ${gmtOffset}`
   }
 }
+
+/**
+ * 判断是否是欧洲地区
+ */
+export const isEuropean = () => {
+  const userLanguage = navigator.language || 'en-US'
+  const europeanLocales = ['de', 'fr', 'it', 'es', 'nl', 'pl'] // 例:德语、法语、西班牙语等
+  return europeanLocales.some((locale) => userLanguage.startsWith(locale))
+}
+
+/**
+ * 根据传入的地区 格式化数字
+ * @param number - 需要格式化的数字
+ * @param digits - 小数位数
+ * @param isEuropean - 是否为欧洲地区
+ */
+export const formatNumber = (number: number, digits: number = 2, isEuropean?: boolean): string => {
+  const userLanguage = isEuropean ? 'de-DE' : 'zh-CN'
+
+  // 设置数字格式化的选项
+  const options: Intl.NumberFormatOptions = {
+    style: 'decimal',
+    minimumFractionDigits: digits
+    // maximumFractionDigits: 3
+  }
+
+  // 其他地区使用默认格式
+  return new Intl.NumberFormat(userLanguage, options).format(number)
+}
+
+/**
+ * 根据用户地区判断日期格式
+ * @returns {string} - 返回日期格式
+ */
+export const getDateFormat = () => {
+  const userLanguage = navigator.language || 'en-US' // 获取浏览器的语言设置
+  // 判断用户地区
+  if (userLanguage === 'en-US') {
+    return 'MM/DD/YYYY' // 美国使用 MM/DD/YYYY 格式
+  } else if (
+    userLanguage.startsWith('de') ||
+    userLanguage.startsWith('fr') ||
+    userLanguage.startsWith('it') ||
+    userLanguage.startsWith('es') ||
+    userLanguage.startsWith('pl') ||
+    userLanguage.startsWith('nl') ||
+    userLanguage.startsWith('pt') ||
+    userLanguage.startsWith('se')
+  ) {
+    return 'DD/MM/YYYY' // 其他欧洲国家(例:德语、法语、西班牙语等)使用 DD/MM/YYYY 格式
+  } else if (['zh-CN', 'ja-JP', 'ko-KR'].includes(userLanguage)) {
+    return 'YYYY-MM-DD' // 东亚国家(如简体中文、日语、韩语)使用 YYYY-MM-DD 格式
+  } else {
+    return 'DD/MM/YYYY' // 其他地区默认 DD/MM/YYYY 格式
+  }
+}

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

@@ -4,7 +4,7 @@ import BasicInformation from './components/BasicInformation.vue'
 import ContainersView from './components/ContainersView.vue'
 import EmailView from './components/EmailView.vue'
 import { cloneDeep } from 'lodash'
-import { transportationMode } from '@/components/TransportationMode'
+import { transportationMode } from '@/components/transportationMode'
 import { useRoute } from 'vue-router'
 import { useOverflow } from '@/hooks/useOverflow'
 import { formatTimezone } from '@/utils/tools'

+ 12 - 3
src/views/Booking/src/components/BookingDetail/src/components/BasicInformation.vue

@@ -2,6 +2,7 @@
 import { useRouter } from 'vue-router'
 import XEClipboard from 'xe-clipboard'
 import AddReferenceDialog from './AddReferenceDialog.vue'
+import { formatNumber } from '@/utils/tools'
 
 const router = useRouter()
 
@@ -108,6 +109,14 @@ const allData: any = ref({
   ]
 })
 
+// 从开始位置截取字符串,并格式化数字(因为后端接口返回的内容带有字符串,所以需要截取)
+const substringFromStart = (str: string, start: number) => {
+  if (!str) {
+    return formatNumber(0, 3)
+  }
+  return formatNumber(Number(str.slice(0, start)), 3)
+}
+
 const convertData = (data: any) => {
   return {
     basicInformation: {
@@ -186,15 +195,15 @@ const convertData = (data: any) => {
       },
       {
         label: 'G. Weight',
-        content: data.packing['G. Weight'] || '--'
+        content: substringFromStart(data.packing['G. Weight'], -4) + ' KGS'
       },
       {
         label: 'Ch. Weight',
-        content: data.packing['Ch. Weight'] || '--'
+        content: substringFromStart(data.packing['Ch. Weight'], -4) + ' KGS'
       },
       {
         label: 'Volume',
-        content: data.packing['Volume'] || '--'
+        content: substringFromStart(data.packing['Volume'], -4) + ' CBM'
       }
     ],
     marksAndDescription: [

+ 6 - 11
src/views/Booking/src/components/BookingDetail/src/components/ContainersView.vue

@@ -1,8 +1,8 @@
 <script setup lang="ts">
-import dayjs from 'dayjs'
 import { type VxeGridInstance, type VxeGridProps } from 'vxe-table'
-import { autoWidth } from '@/utils/table'
+// import { autoWidth } from '@/utils/table'
 import { useRowClickStyle } from '@/hooks/rowClickStyle'
+import { formatTimezone, formatNumber } from '@/utils/tools'
 
 const props = defineProps({
   data: Object
@@ -35,20 +35,15 @@ const handleColumns = (columns: any) => {
     }
 
     // 格式化
-    if (item.formatter === 'date') {
+    if (item.formatter === 'date' || item.formatter === 'dateTime') {
       curColumn = {
         ...curColumn,
-        sortBy: ({ row, column }: any) => {
-          return dayjs(row[column.field]).unix()
-        },
-        formatter: ({ cellValue }: any) =>
-          cellValue ? dayjs(cellValue).format('MMM-DD-YYYY ') : '--'
+        formatter: ({ cellValue }: any) => formatTimezone(cellValue)
       }
-    } else if (item.formatter === 'dateTime') {
+    } else if (item.formatter === 'number') {
       curColumn = {
         ...curColumn,
-        formatter: ({ cellValue }: any) =>
-          cellValue ? dayjs(cellValue).format('MMM-DD-YYYY HH:mm:ss') : '--'
+        formatter: ({ cellValue }: any) => formatNumber(Number(cellValue), item?.digits)
       }
     }
     return curColumn

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

@@ -2,7 +2,7 @@
 import '@wangeditor/editor/dist/css/style.css'
 import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
 import { i18nChangeLanguage, DomEditor } from '@wangeditor/editor'
-import dayjs from 'dayjs'
+import { formatTimezone } from '@/utils/tools'
 
 i18nChangeLanguage('en')
 
@@ -152,7 +152,7 @@ const sendEmail = () => {
         emailRecords.value = res.data.emailRecords
       }
     })
-    .catch((err: any) => {
+    .catch(() => {
       ElMessage.error('Failed to send email')
     })
 }
@@ -221,7 +221,7 @@ const sendEmail = () => {
             <div>{{ item.name?.slice(0, 1) }}</div>
           </div>
           <div class="name">{{ item.name }}</div>
-          <div class="date">{{ dayjs(item.creatTime).format('MM-DD-YYYY HH:mm:ss') }}</div>
+          <div class="date">{{ formatTimezone(item.creatTime) }}</div>
         </div>
         <div class="content">
           {{ item.content }}

+ 6 - 7
src/views/Booking/src/components/BookingTable/src/BookingTable.vue

@@ -6,9 +6,10 @@ import { useRowClickStyle } from '@/hooks/rowClickStyle'
 import dayjs from 'dayjs'
 import { ref, onMounted } from 'vue'
 import { useRouter } from 'vue-router'
-import { transportationMode } from '@/components/TransportationMode'
+import { transportationMode } from '@/components/transportationMode'
 import { useThemeStore } from '@/stores/modules/theme'
 import { useVisitedRowState } from '@/stores/modules/visitedRow'
+import { formatTimezone, formatNumber } from '@/utils/tools'
 
 const visitedRowState = useVisitedRowState()
 const themeStore = useThemeStore()
@@ -58,17 +59,15 @@ const handleColumns = (columns: any, status?: string) => {
       }
     }
     // 格式化
-    if (item.formatter === 'date') {
+    if (item.formatter === 'date' || item.formatter === 'dateTime') {
       curColumn = {
         ...curColumn,
-        formatter: ({ cellValue }: any) =>
-          cellValue ? dayjs(cellValue).format('MMM-DD-YYYY ') : '--'
+        formatter: ({ cellValue }: any) => formatTimezone(cellValue)
       }
-    } else if (item.formatter === 'dateTime') {
+    } else if (item.formatter === 'number') {
       curColumn = {
         ...curColumn,
-        formatter: ({ cellValue }: any) =>
-          cellValue ? dayjs(cellValue).format('MMM-DD-YYYY HH:mm:ss') : '--'
+        formatter: ({ cellValue }: any) => formatNumber(Number(cellValue), item?.digits)
       }
     }
     return curColumn

+ 5 - 1
src/views/Dashboard/src/components/ScoringSystem.vue

@@ -11,6 +11,10 @@ import hhhPng2 from '../image/hhh_2.png'
 import happyPng from '../image/score_happy.png'
 import happyPng2 from '../image/happy_2.png'
 import submitsucessful from '../image/submit_successful.png'
+import { useUserStore } from '@/stores/modules/user'
+
+const userStore = useUserStore()
+
 const dialogVisible = ref(false)
 const innerVisible = ref(false)
 const isShowAngry = ref(false)
@@ -235,7 +239,7 @@ const changeSmileRadio = (title: any, value: any) => {
 }
 // 提交details
 const submitDetails = (val: any) => {
-  const username = localStorage.getItem('account') ? localStorage.getItem('account') : ''
+  const username = userStore.userName
   if (checkboxGroup1.value.length) {
     $api
       .scoringgrade({

+ 48 - 13
src/views/Layout/src/components/Header/HeaderView.vue

@@ -8,6 +8,8 @@ import { useHeaderSearch } from '@/stores/modules/headerSearch'
 import { onBeforeRouteUpdate } from 'vue-router'
 import { useLoadingState } from '@/stores/modules/loadingState'
 import { useThemeStore } from '@/stores/modules/theme'
+import NotificationDrawer from './components/NotificationDrawer.vue'
+import TrainingCard from './components/TrainingCard.vue'
 
 const themeStore = useThemeStore()
 const userStore = useUserStore()
@@ -15,7 +17,6 @@ const route = useRoute()
 const router = useRouter()
 const headerSearch = useHeaderSearch()
 
-const themePopoverRef = ref()
 // 切换系统主题颜色
 const toggleThemeMode = (theme: string) => {
   themeStore.toggleTheme(theme, true)
@@ -30,7 +31,7 @@ const handleSearch = () => {
   }
   // 先判断是否登录
   // 未登录
-  if (!localStorage.getItem('username')) {
+  if (!userStore.userInfo?.uname) {
     $api.getPublicTrackingDetail({ reference_number: searchValue.value }).then((res) => {
       if (res.code === 200) {
         const { data } = res
@@ -157,11 +158,14 @@ const togglePopover = () => {
 const closePopover = () => {
   isPopoverVisible.value = false
 }
+
+const notificationDrawer = ref(false)
 </script>
 
 <template>
   <div class="layout-toolbar">
     <VBreadcrumb></VBreadcrumb>
+    <TrainingCard></TrainingCard>
     <div class="right-info">
       <el-input
         v-model="searchValue"
@@ -173,7 +177,22 @@ const closePopover = () => {
           <span style="margin-top: -1px" class="font_family icon-icon_search_b"></span>
         </template>
       </el-input>
-      <!-- <span class="font_family icon-icon_notice_b" style="font-size: 18px"></span>
+      <div class="notice-icon" v-if="userStore.userInfo?.uname">
+        <!-- <span
+          @click="notificationDrawer = true"
+          class="font_family icon-icon_notice_b"
+          style="font-size: 18px"
+        ></span> -->
+
+        <el-button
+          style="height: 40px; width: 40px; margin-right: 0px"
+          class="el-button--text"
+          @click="notificationDrawer = true"
+        >
+          <span class="font_family icon-icon_notice_b" style="font-size: 18px"></span>
+        </el-button>
+      </div>
+      <!-- 
       <span class="font_family icon-icon_language_b" style="font-size: 16px"></span> -->
       <el-popover
         placement="bottom-end"
@@ -256,9 +275,9 @@ const closePopover = () => {
       >
         <div class="title">
           <div class="avatar">
-            <span>{{ userStore.username?.slice(0, 1) }}</span>
+            <span>{{ userStore.userName?.slice(0, 1) }}</span>
           </div>
-          <span class="name">{{ userStore.username }}</span>
+          <span class="name">{{ userStore.userName }}</span>
         </div>
         <div class="options">
           <div class="item" @click="handleChangePassword">
@@ -275,16 +294,19 @@ const closePopover = () => {
           </div>
         </div>
         <template #reference>
-          <div class="header-avatar" v-if="userStore.username && userStore.isFirstLogin !== true">
-            <div>{{ userStore.username?.slice(0, 1) }}</div>
+          <div class="header-avatar" v-if="userStore.userName && userStore.isFirstLogin !== true">
+            <div>{{ userStore.userName.slice(0, 1) }}</div>
           </div>
         </template>
       </el-popover>
       <el-button
         :class="{ 'el-button--pain-theme': themeStore.theme === 'dark' }"
-        v-if="!userStore.username || (userStore.username && userStore.isFirstLogin === true)"
+        v-if="
+          !userStore.userInfo?.uname ||
+          (userStore.userInfo?.uname && userStore.isFirstLogin === true)
+        "
         class="el-button--main"
-        style="padding: 8px 10px; margin-right: 10px; margin-left: 0"
+        style="padding: 8px 10px; margin-right: 20px; margin-left: 0"
         plain
         @click="handleDownload"
       >
@@ -292,7 +314,10 @@ const closePopover = () => {
         Download KLN Portal
       </el-button>
       <el-button
-        v-if="!userStore.username || (userStore.username && userStore.isFirstLogin === true)"
+        v-if="
+          !userStore.userInfo?.uname ||
+          (userStore.userInfo?.uname && userStore.isFirstLogin === true)
+        "
         class="el-button--main"
         style="margin-left: -10px"
         @click="handleLogin"
@@ -302,6 +327,7 @@ const closePopover = () => {
     <DownloadKLNPortal ref="downloadKLNPortalRef"></DownloadKLNPortal>
     <ChangePasswordDialog ref="changePasswordDialogRef"></ChangePasswordDialog>
     <LogoutDialog ref="logoutDialogRef"></LogoutDialog>
+    <NotificationDrawer v-model="notificationDrawer"></NotificationDrawer>
   </div>
 </template>
 
@@ -309,6 +335,7 @@ const closePopover = () => {
 .header-avatar {
   width: 24px;
   height: 24px;
+  margin-left: 8px;
   text-align: center;
   border-radius: 50%;
   background-color: var(--color-theme);
@@ -326,15 +353,15 @@ const closePopover = () => {
 .layout-toolbar {
   display: flex;
   justify-content: space-between;
+  position: relative;
   height: 100%;
 }
 .right-info {
   display: flex;
   align-items: center;
   justify-content: center;
-  gap: 8px;
   height: 100%;
-
+  gap: 8px;
   .el-input {
     height: 32px;
     width: 400px;
@@ -353,6 +380,14 @@ const closePopover = () => {
       height: 32px;
     }
   }
+
+  .notice-icon {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    margin-top: 2px;
+    cursor: pointer;
+  }
 }
 </style>
 <style lang="scss">
@@ -495,4 +530,4 @@ div.el-popover.el-popper.user-config-popover {
 div.el-popper.theme-popper-class {
   padding: 3px 4px;
 }
-</style>
+</style>

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

@@ -0,0 +1,268 @@
+<script setup lang="ts">
+import NotificationMessageCard from '@/components/NotificationMessageCard/src/NotificationMessageCard.vue'
+
+const drawerModel = defineModel('drawerModel', { type: Boolean, default: false })
+const notificationType = ref('all')
+const notificationTypeList = ref([
+  {
+    label: 'All Notifications',
+    value: 'all'
+  },
+  {
+    label: 'Milestone Update',
+    value: 'feature1'
+  },
+  {
+    label: 'Container Status Update',
+    value: 'all2'
+  },
+  {
+    label: 'Departure/Arrival Delay',
+    value: 'feature3'
+  },
+  {
+    label: 'ETD/ETA Change',
+    value: 'all4'
+  },
+  {
+    label: 'Feature Update',
+    value: 'feature5'
+  }
+])
+
+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.'
+    }
+  }
+]
+</script>
+
+<template>
+  <el-drawer class="notice-drawer" v-model="drawerModel" size="432px">
+    <template #header>
+      <el-select size="large" v-model="notificationType" class="notification-type">
+        <el-option
+          v-for="item in notificationTypeList"
+          :key="item.value"
+          :label="item.label"
+          :value="item.value"
+        />
+      </el-select>
+    </template>
+    <template #default>
+      <div class="notification-header">
+        <div class="mark-all-read">
+          <span class="font_family icon-icon_confirm_b show-icon"></span>
+          <span>Mark all read</span>
+        </div>
+        <div class="view-all">
+          <span class="font_family icon-icon_confirm_b show-icon"></span>
+          <span>View all</span>
+          <span class="font_family icon-icon_administration_b setting"></span>
+        </div>
+      </div>
+      <div class="notification-content">
+        <NotificationMessageCard :data="notificationList" />
+      </div>
+    </template>
+  </el-drawer>
+</template>
+<style lang="scss" scoped>
+div.layout-toolbar {
+  .notification-content {
+    padding: 16px;
+  }
+  .password-notifications {
+    margin-bottom: 16px;
+    .header {
+      display: flex;
+      align-items: center;
+      margin-bottom: 8px;
+      .status-icon {
+        width: 8px;
+        height: 8px;
+        border-radius: 50%;
+        background-color: var(--color-theme);
+        margin-right: 10px;
+      }
+      .title {
+        font-weight: 700;
+      }
+    }
+    .card-content {
+      padding: 16px 8px 24px;
+      background: linear-gradient(180deg, #ffe294 0%, #f6f8fa 100%);
+      border-radius: 12px;
+      &.is-expired {
+        background: linear-gradient(182deg, #ef99a0 2.2%, #f6f8fa 98.77%);
+      }
+      .title {
+        span {
+          vertical-align: middle;
+          font-weight: 700;
+        }
+        .font_family {
+          margin-right: 8px;
+        }
+      }
+      .details {
+        margin: 8px 0 16px 24px;
+      }
+    }
+  }
+  .feature-update {
+    margin-bottom: 16px;
+
+    .header {
+      display: flex;
+      align-items: center;
+      margin-bottom: 8px;
+      .status-icon {
+        width: 8px;
+        height: 8px;
+        border-radius: 50%;
+        background-color: var(--color-theme);
+        margin-right: 10px;
+      }
+      .title {
+        font-weight: 700;
+      }
+    }
+    .card-content {
+      padding: 16px 8px 24px;
+      background: linear-gradient(137deg, #fff4eb 12.41%, #f0f3ff 52.63%, #e0f7f9 93.28%);
+      border-radius: 12px;
+      .title {
+        img {
+          vertical-align: middle;
+        }
+        span {
+          vertical-align: middle;
+          display: inline-block;
+          font-weight: 700;
+        }
+        .font_family {
+          margin-right: 8px;
+        }
+      }
+      .details {
+        margin: 8px 0 16px 24px;
+      }
+    }
+  }
+}
+</style>
+<style lang="scss">
+div.layout-toolbar {
+  .el-overlay:has(> .notice-drawer) {
+    background-color: transparent;
+  }
+  .notice-drawer {
+    top: 48px;
+    height: calc(100% - 48px);
+    .el-drawer__header {
+      gap: 52px;
+      .el-drawer__close {
+        color: var(--color-neutral-1);
+        font-size: 16px;
+      }
+    }
+    .el-drawer__body {
+      padding: 0;
+    }
+    .notification-type {
+      .el-select__wrapper {
+        width: 320px !important;
+      }
+    }
+    .notification-header {
+      display: flex;
+      justify-content: space-between;
+      position: sticky;
+      top: 0;
+      height: 40px;
+      padding: 0 16px;
+      line-height: 40px;
+      background-color: white;
+      border-bottom: 1px solid var(--color-border);
+      .mark-all-read {
+        span {
+          color: var(--color-theme);
+          vertical-align: middle;
+        }
+        .show-icon {
+          margin-right: 2px;
+        }
+      }
+      .view-all {
+        span {
+          vertical-align: middle;
+        }
+        .show-icon {
+          margin-right: 2px;
+        }
+        .setting {
+          margin-left: 16px;
+        }
+      }
+    }
+  }
+}
+</style>

+ 199 - 0
src/views/Layout/src/components/Header/components/TrainingCard.vue

@@ -0,0 +1,199 @@
+<script setup lang="ts">
+import NotificationMessageCard from '@/components/NotificationMessageCard/src/NotificationMessageCard.vue'
+
+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 curCard = computed(() => {
+  return notificationList[curIndex.value] || null
+})
+const curIndex = ref(0)
+// 设置定时器进行自动轮播
+let intervalId = null
+
+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)
+    result = false
+  }
+
+  // 如果到达最后一个消息,设置为null以清除显示
+  if (curIndex.value >= notificationList.length) {
+    clearInterval(intervalId)
+    // curCard.value = null
+    result = false
+  }
+  return result
+}
+
+const initTrainingCard = () => {
+  if (curCard.value?.notificationType === 'event') {
+    intervalId = setInterval(nextNotification, 2000)
+  }
+}
+initTrainingCard()
+
+const closeMessage = () => {
+  // 如果当前消息为event类型,则需先清除定时器
+  if (curCard.value?.notificationType === 'event') {
+    clearInterval(intervalId)
+  }
+
+  const result = nextNotification()
+  if (result) {
+    intervalId = setInterval(nextNotification, 2000)
+  }
+}
+</script>
+
+<template>
+  <div class="training-card" v-if="curCard">
+    <el-button
+      @click="closeMessage"
+      style="height: 18px; width: 18px"
+      class="el-button--text close-icon"
+    >
+      <span class="font_family icon-icon_reject_b"></span>
+    </el-button>
+    <NotificationMessageCard v-if="curCard" :data="[curCard]"></NotificationMessageCard>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.training-card {
+  position: absolute;
+  top: 60px;
+  right: 20px;
+  z-index: 2010;
+  width: 432px;
+  padding: 16px;
+  padding-bottom: 0;
+  background-color: #fff;
+  border-radius: 12px;
+  box-shadow: 4px 4px 16px 0px rgba(0, 0, 0, 0.1);
+  .close-icon {
+    position: absolute;
+    top: 14px;
+    right: 16px;
+    padding-top: 2px;
+    padding-bottom: 0px;
+    font-size: 18px;
+    cursor: pointer;
+  }
+}
+</style>

+ 45 - 7
src/views/Layout/src/components/Menu/MenuView.vue

@@ -11,17 +11,55 @@ const isCollapse = defineModel<boolean>()
 
 const menuList = ref()
 watch(
-  () => userStore.username,
+  () => userStore.userInfo?.uname,
   () => {
     getMenuList()
   }
 )
 const getMenuList = () => {
-  $api.getMenuList().then((res) => {
-    if (res.code === 200) {
-      menuList.value = res.data
+  // $api.getMenuList().then((res) => {
+  //   if (res.code === 200) {
+  //     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: 'Operation Log',
+          path: '/Operationlog'
+        },
+        {
+          index: '4-2',
+          label: 'System Message',
+          path: '/system-message'
+        }
+      ]
     }
-  })
+  ]
 }
 getMenuList()
 //监听窗口大小
@@ -46,7 +84,7 @@ const whiteList = ['/login', '/public-tracking', '/public-tracking/detail', '/re
 // 判断是否允许跳转
 const isAllowJump = (path: any) => {
   // 判断是否登录
-  if (!whiteList.includes(path) && !localStorage.getItem('username')) {
+  if (!whiteList.includes(path) && !userStore.userInfo?.uname) {
     ElMessage.warning({
       message: 'Please log in to use this feature.',
       grouping: true
@@ -86,7 +124,7 @@ const changeRouter = (path: any) => {
   emits('changeVisible', isVisible.value)
   isVisible.value = false
   let toPath = path
-  if (path === '/tracking' && !localStorage.getItem('username')) {
+  if (path === '/tracking' && !userStore.userInfo?.uname) {
     toPath = '/public-tracking'
   }
   // 如果允许跳转,执行跳转

+ 1 - 4
src/views/Login/src/components/ChangePasswordCard.vue

@@ -21,7 +21,7 @@ const loginForm = ref({
   newPassword: '',
   confirmPassword: ''
 })
-loginForm.value.username = localStorage.getItem('username') || ''
+loginForm.value.username = userStore.userInfo?.uname || ''
 if (!loginForm.value.username) {
   router.push({
     name: 'Login'
@@ -321,9 +321,6 @@ onUnmounted(() => {
       background-color: transparent;
     }
     &.is-disabled {
-      :deep(.el-input__wrapper) {
-        // background-color: #f4f4f4;
-      }
       :deep(.el-input__inner) {
         -webkit-text-fill-color: var(--color-neutral-1);
         color: var(--color-neutral-1);

+ 4 - 3
src/views/Login/src/loginView.vue

@@ -210,8 +210,9 @@ const handleResult = (res: any) => {
         }
       })
     }
-    userStore.setUsername(res.data.uname || '')
+    userStore.setUserInfo(res.data?.user_info || {})
     router.push('/')
+    localStorage.setItem('showTrainingCardAfterLogin', 'true')
   } else if (res.code === 400) {
     const { data } = res
     if (data.msg === 'passwordExpires') {
@@ -220,12 +221,12 @@ const handleResult = (res: any) => {
         type: 'warning',
         confirmButtonClass: 'el-button--dark'
       })
-      userStore.setUsername(res.data.uname || '')
+      userStore.setUserInfo(res.data?.user_info || {})
       router.push({
         name: 'Reset Password'
       })
     } else if (data.msg === 'First login, please change your password') {
-      userStore.setUsername(res.data.uname, true)
+      userStore.setUserInfo(res.data?.user_info || {}, true)
       firstLoginTipsRef.value.openDialog()
     }
   }

+ 4 - 10
src/views/OperationLog/src/components/BookingTable/src/BookingTable.vue

@@ -2,10 +2,11 @@
 import { ref, nextTick, onMounted } from 'vue'
 import { type VxeGridInstance, type VxeGridProps } from 'vxe-table'
 import DownloadDialog from './components/DownloadDialog.vue'
-import { autoWidth } from '@/utils/table'
+// import { autoWidth } from '@/utils/table'
 import { useRowClickStyle } from '@/hooks/rowClickStyle'
 import dayjs from 'dayjs'
 import { useThemeStore } from '@/stores/modules/theme'
+import { formatTimezone } from '@/utils/tools'
 
 const themeStore = useThemeStore()
 
@@ -46,17 +47,10 @@ const handleColumns = (columns: any, status?: string) => {
       }
     }
     // 格式化
-    if (item.formatter === 'date') {
+    if (item.formatter === 'date' || item.formatter === 'dateTime') {
       curColumn = {
         ...curColumn,
-        formatter: ({ cellValue }: any) =>
-          cellValue ? dayjs(cellValue).format('MMM-DD-YYYY ') : '--'
-      }
-    } else if (item.formatter === 'dateTime') {
-      curColumn = {
-        ...curColumn,
-        formatter: ({ cellValue }: any) =>
-          cellValue ? dayjs(cellValue).format('YYYY/MM/DD HH:mm:ss') : '--'
+        formatter: ({ cellValue }: any) => formatTimezone(cellValue)
       }
     }
     return curColumn

+ 0 - 510
src/views/OperationLog/src/components/BookingTable/src/BookingTableColumns.ts

@@ -1,510 +0,0 @@
-import dayjs from 'dayjs'
-
-const BookingTableColumns: any = [
-  {
-    field: 'booking_no',
-    title: 'Booking No.',
-    slots: {
-      default: 'bookingNo'
-    },
-    type: 'link'
-  },
-  {
-    field: 'm_bol',
-    title: 'MBOL No.'
-  },
-  {
-    field: 'h_bol',
-    title: 'HBOL No.'
-  },
-  {
-    field: 'po_no',
-    title: 'PO No.'
-  },
-  {
-    field: 'quote_no',
-    title: 'Quote No.'
-  },
-  {
-    field: 'carrier_booking',
-    title: 'Carrier Booking No.'
-  },
-  {
-    field: 'contract',
-    title: 'Contract No.'
-  },
-  {
-    field: 'mode',
-    title: 'Transportation Mode',
-    type: 'mode',
-    slots: {
-      default: 'mode'
-    }
-  },
-  {
-    field: 'status',
-    title: 'Status',
-    type: 'status',
-    slots: {
-      default: 'status'
-    }
-  },
-  {
-    field: 'shipper',
-    title: 'Shipper'
-  },
-  {
-    field: 'consignee',
-    title: 'Consignee'
-  },
-  {
-    field: 'origin',
-    title: 'Origin Agent'
-  },
-  {
-    field: 'agent',
-    title: 'Destination Agent'
-  },
-  {
-    field: 'sales_rep',
-    title: 'Sales'
-  },
-  {
-    field: 'created_time',
-    title: 'Creation Time',
-    formatter: ({ cellValue }: any) => {
-      return cellValue ? dayjs(cellValue).format('MMM-DD-YYYY HH:mm:ss') : '--'
-    }
-  },
-  {
-    field: 'confirmation_time',
-    title: 'Confirmation Time',
-    formatter: ({ cellValue }: any) => {
-      return cellValue ? dayjs(cellValue).format('MMM-DD-YYYY HH:mm:ss') : '--'
-    }
-  },
-  {
-    field: 'f_etd',
-    title: 'ETD',
-    formatter: ({ cellValue }: any) => {
-      return cellValue ? dayjs(cellValue).format('MMM-DD-YYYY') : '--'
-    }
-  },
-  {
-    field: 'f_eta',
-    title: 'ETA',
-    formatter: ({ cellValue }: any) => {
-      return cellValue ? dayjs(cellValue).format('MMM-DD-YYYY') : '--'
-    }
-  },
-  {
-    field: 'place_of_receipt_exp',
-    title: 'Place of Receipt'
-  },
-  {
-    field: 'fport_of_loading_exp',
-    title: 'Port of Loading'
-  },
-  {
-    field: 'place_of_delivery_exp',
-    title: 'Place of Delivery'
-  },
-  {
-    field: 'final_desination_exp',
-    title: 'Destination'
-  },
-  {
-    field: 'from_station',
-    title: 'Origin'
-  },
-  {
-    field: 'f_carrier',
-    title: 'Carrier'
-  },
-  {
-    field: 'f_voyage',
-    title: 'Voyage'
-  },
-  {
-    field: 'f_vessel',
-    title: 'Vessel'
-  },
-  {
-    field: 'week',
-    title: 'Week'
-  },
-  {
-    field: 'd20',
-    title: 'D20'
-  },
-  {
-    field: 'sd40',
-    title: 'D40'
-  },
-  {
-    field: 'd45',
-    title: 'D45'
-  },
-  {
-    field: 'rd40',
-    title: 'RD'
-  },
-  {
-    field: 'hq40',
-    title: 'HQ'
-  },
-  {
-    field: 'created_by',
-    title: 'Created by'
-  },
-  {
-    field: 'terms',
-    title: 'Other info'
-  }
-]
-
-const addOtherConfig = () => {
-  BookingTableColumns.forEach((item: any) => {
-    item.sortable = true
-  })
-}
-addOtherConfig()
-
-const defaultColumns = [
-  'Transportation Mode',
-  'Status',
-  'Booking No.',
-  'MBOL No.',
-  'HBOL No.',
-  'PO No.',
-  'Shipper',
-  'Consignee',
-  'Origin',
-  'Destination',
-  'Creation Time',
-  'ETD',
-  'ETA',
-  'Week',
-  'Vessel',
-  'Voyage',
-  'Created by',
-  'Other info'
-]
-
-// 分组
-const groupColumns = [
-  {
-    name: 'All',
-    children: [
-      {
-        label: 'Booking No.',
-        field: 'booking_no'
-      },
-      {
-        label: 'MBOL No.',
-        field: 'm_bol'
-      },
-      {
-        label: 'HBOL No.',
-        field: 'h_bol'
-      },
-      {
-        label: 'PO No.',
-        field: 'po_no'
-      },
-      {
-        label: 'Quote No.',
-        field: 'quote_no'
-      },
-      {
-        label: 'Carrier Booking No.',
-        field: 'carrier_booking'
-      },
-      {
-        label: 'Contract No.',
-        field: 'contract'
-      },
-      {
-        label: 'Transportation Mode',
-        field: 'mode'
-      },
-      {
-        label: 'Status',
-        field: 'status'
-      },
-      {
-        label: 'Shipper',
-        field: 'shipper'
-      },
-      {
-        label: 'Consignee',
-        field: 'consignee'
-      },
-      {
-        label: 'Origin Agent',
-        field: 'origin'
-      },
-      {
-        label: 'Destination Agent',
-        field: 'agent'
-      },
-      {
-        label: 'Sales',
-        field: 'sales_rep'
-      },
-      {
-        label: 'Creation Time',
-        field: 'created_time'
-      },
-      {
-        label: 'Confirmation Time',
-        field: 'confirmation_time'
-      },
-      {
-        label: 'ETD',
-        field: 'f_etd'
-      },
-      {
-        label: 'ETA',
-        field: 'f_eta'
-      },
-      {
-        label: 'Place of Receipt',
-        field: 'place_of_receipt_exp'
-      },
-      {
-        label: 'Port of Loading',
-        field: 'fport_of_loading_exp'
-      },
-      {
-        label: 'Place of Delivery',
-        field: 'place_of_delivery_exp'
-      },
-      {
-        label: 'Destination',
-        field: 'final_desination_exp'
-      },
-      {
-        label: 'Origin',
-        field: 'from_station'
-      },
-      {
-        label: 'Carrier',
-        field: 'f_carrier'
-      },
-      {
-        label: 'Voyage',
-        field: 'f_voyage'
-      },
-      {
-        label: 'Vessel',
-        field: 'f_vessel'
-      },
-      {
-        label: 'Week',
-        field: 'week'
-      },
-      {
-        label: 'D20',
-        field: 'd20'
-      },
-      {
-        label: 'D40',
-        field: 'sd40'
-      },
-      {
-        label: 'D45',
-        field: 'd45'
-      },
-      {
-        label: 'RD',
-        field: 'rd40'
-      },
-      {
-        label: 'HQ',
-        field: 'hq40'
-      },
-      {
-        label: 'Created by',
-        field: 'created_by'
-      },
-      {
-        label: 'Other info',
-        field: 'terms'
-      }
-    ]
-  },
-  {
-    name: 'Reference No.',
-    children: [
-      {
-        label: 'Booking No.',
-        field: 'booking_no'
-      },
-      {
-        label: 'MBOL No.',
-        field: 'm_bol'
-      },
-      {
-        label: 'HBOL No.',
-        field: 'h_bol'
-      },
-      {
-        label: 'PO No.',
-        field: 'po_no'
-      },
-      {
-        label: 'Quote No.',
-        field: 'quote_no'
-      },
-      {
-        label: 'Carrier Booking No.',
-        field: 'carrier_booking'
-      },
-      {
-        label: 'Contract No.',
-        field: 'contract'
-      }
-    ]
-  },
-  {
-    name: 'General',
-    children: [
-      {
-        label: 'Transportation Mode',
-        field: 'mode'
-      },
-      {
-        label: 'Status',
-        field: 'status'
-      }
-    ]
-  },
-  {
-    name: 'Parties',
-    children: [
-      {
-        label: 'Shipper',
-        field: 'shipper'
-      },
-      {
-        label: 'Consignee',
-        field: 'consignee'
-      },
-      {
-        label: 'Origin Agent',
-        field: 'origin'
-      },
-      {
-        label: 'Destination Agent',
-        field: 'agent'
-      },
-      {
-        label: 'Sales',
-        field: 'sales_rep'
-      }
-    ]
-  },
-  {
-    name: 'Time',
-    children: [
-      {
-        label: 'Creation Time',
-        field: 'created_time'
-      },
-      {
-        label: 'Confirmation Time',
-        field: 'confirmation_time'
-      },
-      {
-        label: 'ETD',
-        field: 'f_etd'
-      },
-      {
-        label: 'ETA',
-        field: 'f_eta'
-      }
-    ]
-  },
-  {
-    name: 'Places',
-    children: [
-      {
-        label: 'Place of Receipt',
-        field: 'place_of_receipt_exp'
-      },
-      {
-        label: 'Port of Loading',
-        field: 'fport_of_loading_exp'
-      },
-      {
-        label: 'Place of Delivery',
-        field: 'place_of_delivery_exp'
-      },
-      {
-        label: 'Destination',
-        field: 'final_desination_exp'
-      },
-      {
-        label: 'Origin',
-        field: 'from_station'
-      }
-    ]
-  },
-  {
-    name: 'Transportation',
-    children: [
-      {
-        label: 'Carrier',
-        field: 'f_carrier'
-      },
-      {
-        label: 'Voyage',
-        field: 'voyage_m_voyage'
-      },
-      {
-        label: 'Vessel',
-        field: 'vessel_m_vessel'
-      },
-      {
-        label: 'Week',
-        field: 'week'
-      },
-      {
-        label: 'D20',
-        field: 'd20'
-      },
-      {
-        label: 'D40',
-        field: 'sd40'
-      },
-      {
-        label: 'D45',
-        field: 'd45'
-      },
-      {
-        label: 'RD',
-        field: 'rd40'
-      },
-      {
-        label: 'HQ',
-        field: 'hq40'
-      }
-    ]
-  },
-  {
-    name: 'Others',
-    children: [
-      {
-        label: 'Created by',
-        field: 'created_by'
-      },
-      {
-        label: 'Other info',
-        field: 'terms'
-      }
-    ]
-  }
-]
-
-export { BookingTableColumns, defaultColumns, groupColumns }

+ 1 - 0
src/views/SystemMessage/index.ts

@@ -0,0 +1 @@
+export { default } from './src/SystemMessage.vue'

+ 259 - 0
src/views/SystemMessage/src/SystemMessage.vue

@@ -0,0 +1,259 @@
+<script setup lang="ts">
+import EventCard from '@/components/NotificationMessageCard/src/components/EventCard.vue'
+import PersonalProfile from './components/PersonalProfile.vue'
+
+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 activeItem = ref('Milestone Update')
+const setActiveItem = (item: string) => {
+  activeItem.value = item
+}
+
+const activeName = ref('first')
+
+const handleClick = () => {}
+
+const notificationList = [
+  {
+    type: 'milestone',
+    isMultiple: true,
+    numericRecords: 3,
+    isRead: true,
+    title: 'Milestone Update',
+    mode: 'Ocean Freight',
+    no: 'SHJN2301234',
+    tag: 'Booking Confirmed',
+    location: 'Hong Kong',
+    time: 'Jan 10, 2025 14:30 UTC+8'
+  },
+  {
+    type: 'container',
+    isRead: false,
+    mode: '',
+    no: 'SHJN2301234',
+    tag: 'Unloaded From Vessel',
+    location: 'Hong Kong',
+    time: 'Jan 10, 2025 14:30 UTC+8',
+    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',
+    time: 'Jan 10, 2025 14:30 UTC+8',
+    info: {
+      time: 'ATD: Jan 12, 16:30 (+2 days delay)',
+      departureDelayNum: 10,
+      arrivalDelayNum: 8
+    }
+  },
+  {
+    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,
+      time: 'Updated ETD: Jan 17, 15:00'
+    },
+    location: 'Hong Kong',
+    changeTime: 'Updated ETD: Jan 17, 15:00',
+    time: 'Jan 10, 2025 14:30 UTC+8'
+  }
+]
+</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>
+          </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>
+          </template>
+          <PersonalProfile />
+        </el-tab-pane>
+        <el-tab-pane label="Read" name="third">Role</el-tab-pane>
+      </el-tabs>
+    </div>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.Title {
+  display: flex;
+  height: 68px;
+  border-bottom: 1px solid var(--color-border);
+  font-size: var(--font-size-6);
+  font-weight: 700;
+  padding: 0 24px;
+  align-items: center;
+}
+.system-message {
+  display: flex;
+  height: calc(100% - 68px);
+  .count {
+    display: inline-flex;
+    justify-content: center;
+    height: 18px;
+    min-width: 18px;
+    padding-left: 4px;
+    padding-right: 5px;
+    background-color: var(--color-theme);
+    border-radius: 9px;
+    font-size: 12px;
+    line-height: 18px;
+    text-align: center;
+    span {
+      color: var(--color-white);
+      font-weight: 700;
+    }
+  }
+}
+.left-nav {
+  width: 280px;
+  padding: 24px;
+  padding-right: 0;
+  border-right: 1px solid var(--color-border);
+  .el-collapse {
+    padding-right: 16px;
+    border-top: none;
+    :deep(.el-collapse-item__header) {
+      font-weight: 700;
+    }
+  }
+  .collapse-item {
+    display: flex;
+    align-items: center;
+    justify-content: space-between;
+    position: relative;
+    width: 240px;
+    height: 48px;
+    margin-bottom: 4px;
+    padding: 0 16px;
+    border-radius: 12px;
+    &:hover {
+      background-color: var(--color-system-message-nav-bg);
+    }
+    .active-sign {
+      position: absolute;
+      top: 50%;
+      left: 0;
+      transform: translateY(-50%);
+      width: 4px;
+      height: 21px;
+      border-radius: 12px;
+      background-color: var(--color-theme);
+    }
+
+    &.is-active {
+      background-color: var(--color-system-message-nav-bg);
+      & > span {
+        font-weight: 700;
+        color: var(--color-theme);
+      }
+    }
+    &:last-child {
+      margin-bottom: 8px;
+    }
+  }
+  :deep(.el-collapse-item__header) {
+    width: 240px;
+    padding: 16px;
+  }
+}
+
+.right-content {
+  flex: 1;
+  padding-top: 24px;
+  :deep(.el-tabs__nav-scroll) {
+    padding-left: 16px;
+    border-bottom: 1px solid var(--color-border);
+    .el-tabs__item {
+      font-weight: 400;
+      color: var(--color-neutral-1);
+      &.is-active {
+        font-weight: 700;
+      }
+    }
+  }
+  :deep(.el-tabs) {
+    height: calc(100% - 40px);
+    .el-tabs__content {
+      overflow-y: auto;
+    }
+  }
+}
+</style>

+ 348 - 0
src/views/SystemMessage/src/components/PersonalProfile.vue

@@ -0,0 +1,348 @@
+<script setup lang="ts">
+import dayjs from 'dayjs'
+import { isEuropean, getDateFormat } from '@/utils/tools'
+import ChangePasswordDialog from '@/views/Layout/src/components/Header/components/ChangePasswordDialog.vue'
+import { useUserStore } from '@/stores/modules/user'
+
+const userStore = useUserStore()
+const form = reactive({
+  firstName: userStore.userInfo?.first_name,
+  lastName: userStore.userInfo?.last_name,
+  username: userStore.userInfo?.uname,
+  email: userStore.userInfo?.email,
+  password: '**************'
+})
+
+const segmented = ref('dateTime')
+const handleSegmented = (type: string) => {
+  segmented.value = type
+}
+
+const changePasswordDialogRef = ref()
+const handleChangePassword = () => {
+  changePasswordDialogRef.value.openDialog()
+}
+
+const monthMap = {
+  'MMM/DD/YYYY': 'MM/DD/YYYY',
+  'DD/MMM/YYYY': 'DD/MM/YYYY',
+  'YYYY-MMM-DD': 'YYYY-MM-DD'
+}
+// 用户没有指定日期格式化时,通过时区自动判断
+const initDateFormat = () => {
+  return monthMap[userStore.userInfo?.date_format] || getDateFormat()
+}
+const initMonthFormat = () => {
+  return userStore.userInfo?.date_format || getDateFormat()
+}
+const dateFormat = ref(initDateFormat())
+const monthFormat = ref(initMonthFormat())
+const dateFormatExample = computed(() => {
+  return {
+    'MM/DD/YYYY': [
+      {
+        label: dayjs().format('MM/DD/YYYY'),
+        value: 'MM/DD/YYYY'
+      },
+      {
+        label: dayjs().format('MMM/DD/YYYY'),
+        value: 'MMM/DD/YYYY'
+      }
+    ],
+    'DD/MM/YYYY': [
+      {
+        label: dayjs().format('DD/MM/YYYY'),
+        value: 'DD/MM/YYYY'
+      },
+      {
+        label: dayjs().format('DD/MMM/YYYY'),
+        value: 'DD/MMM/YYYY'
+      }
+    ],
+    'YYYY-MM-DD': [
+      {
+        label: dayjs().format('YYYY-MM-DD'),
+        value: 'YYYY-MM-DD'
+      },
+      {
+        label: dayjs().format('YYYY-MMM-DD'),
+        value: 'YYYY-MMM-DD'
+      }
+    ]
+  }
+})
+
+const handleDateFormat = (value: string) => {
+  monthFormat.value = dateFormatExample.value[value][0].value
+}
+
+const numbersFormat = ref(userStore.userInfo?.numbers_format)
+
+// 判断用户是否指定数字格式化,没有指定则自动判断
+const isSpecifyNumbersFormat = () => {
+  numbersFormat.value = isEuropean() ? 'European' : 'US/UK'
+}
+isSpecifyNumbersFormat()
+
+const saveConfig = (model: string) => {
+  let params = {}
+  if (model === 'profile') {
+    params = {
+      save_model: 'profile',
+      first_name: form.firstName,
+      last_name: form.lastName
+    }
+  } else {
+    params = {
+      save_model: 'no_profile',
+      date_fromat: monthFormat.value,
+      number_format: numbersFormat.value
+    }
+  }
+  $api.saveUserInfo(params).then((res: any) => {
+    if (res.cdoe === 200) {
+      console.log(res)
+    }
+  })
+}
+</script>
+
+<template>
+  <div class="personal-profile">
+    <div class="basic-information">
+      <div class="title">Basic Information</div>
+      <div class="content">
+        <div class="row">
+          <div class="item">
+            <p class="label">First Name</p>
+            <el-input size="large" v-model="form.firstName" placeholder="Please enter..." />
+          </div>
+          <div class="item">
+            <p class="label">Last Name</p>
+            <el-input size="large" v-model="form.lastName" placeholder="Please enter..." />
+          </div>
+        </div>
+        <div class="row">
+          <div class="item">
+            <p class="label">User Name</p>
+            <el-input size="large" :disabled="true" v-model="form.username" />
+          </div>
+          <div class="item">
+            <p class="label">Email</p>
+            <el-input size="large" :disabled="true" v-model="form.email" />
+          </div>
+        </div>
+        <div class="row">
+          <div class="item">
+            <p class="label">
+              Password
+              <span style="margin: 0 2px 0 8px" class="font_family icon-icon_time_b"></span>
+              <span>Your password will be expire in</span>
+              <span style="margin-left: 4px; color: var(--color-theme)">4 day(s)</span>
+            </p>
+            <div class="password-change">
+              <el-input
+                size="large"
+                type="password"
+                style="width: 330px"
+                :disabled="true"
+                v-model="form.password"
+              />
+              <el-button @click="handleChangePassword" class="el-button--main" plain size="large"
+                >Change Password</el-button
+              >
+            </div>
+          </div>
+        </div>
+        <div class="row">
+          <el-button @click="saveConfig('profile')" class="el-button--dark save-icon" size="large"
+            >Save</el-button
+          >
+        </div>
+      </div>
+    </div>
+    <div class="personal-preferences">
+      <div class="title">Personal Preferences</div>
+      <div class="segmented">
+        <div
+          style="width: 121px"
+          :class="{ 'is-active': segmented === 'dateTime' }"
+          @click="handleSegmented('dateTime')"
+          class="item"
+        >
+          Date & Time
+        </div>
+        <div
+          style="width: 162px"
+          :class="{ 'is-active': segmented === 'numbersFormat' }"
+          @click="handleSegmented('numbersFormat')"
+          class="item"
+        >
+          Numbers Format
+        </div>
+      </div>
+      <div class="date-format" v-if="segmented === 'dateTime'">
+        <div class="title">Date & Time</div>
+        <div class="content">
+          <el-radio-group v-model="dateFormat" label="string" @change="handleDateFormat">
+            <el-row>
+              <el-col v-for="(list, key) in dateFormatExample" :key="key">
+                ><el-radio :value="key">
+                  <template #default>
+                    <span>{{ key }}</span>
+                    <el-select
+                      style="margin-left: 28px; width: 320px"
+                      v-if="dateFormat === key"
+                      size="large"
+                      v-model="monthFormat"
+                    >
+                      <el-option
+                        v-for="item in list"
+                        :key="item.value"
+                        :label="item.label"
+                        :value="item.value"
+                      ></el-option>
+                    </el-select>
+                  </template>
+                </el-radio>
+              </el-col>
+
+              <el-col>
+                <el-button
+                  @click="saveConfig('no_profile')"
+                  style="padding: 0 40px"
+                  class="el-button--dark save-icon"
+                  size="large"
+                  >Save</el-button
+                >
+              </el-col>
+            </el-row>
+          </el-radio-group>
+        </div>
+      </div>
+      <div class="numbers-format" v-if="segmented === 'numbersFormat'">
+        <div class="title">Numbers Format</div>
+        <div class="content">
+          <el-radio-group v-model="numbersFormat" label="string">
+            <el-row>
+              <el-col
+                ><el-radio value="US/UK">
+                  <template #default>
+                    <span>1,234.56 (US/UK)</span>
+                  </template>
+                </el-radio></el-col
+              >
+              <el-col
+                ><el-radio value="European">
+                  <template #default>
+                    <span>1.234,56 (European)</span>
+                  </template>
+                </el-radio></el-col
+              >
+              <el-col>
+                <el-button style="padding: 0 40px" class="el-button--dark save-icon" size="large"
+                  >Save</el-button
+                >
+              </el-col>
+            </el-row>
+          </el-radio-group>
+        </div>
+      </div>
+    </div>
+
+    <ChangePasswordDialog ref="changePasswordDialogRef"></ChangePasswordDialog>
+  </div>
+</template>
+
+<style scoped lang="scss">
+.personal-profile {
+  .basic-information,
+  .personal-preferences {
+    border: 1px solid var(--color-border);
+    border-radius: 12px;
+    padding: 16px 16px 32px;
+    & > .title {
+      margin-bottom: 21px;
+      font-size: 18px;
+      font-weight: 700;
+    }
+  }
+}
+.basic-information {
+  .content {
+    .row {
+      display: flex;
+      gap: 8px;
+      .item {
+        flex: 1;
+        margin-bottom: 16px;
+        .label {
+          margin-bottom: 4px;
+          font-size: 12px;
+          color: var(--color-neutral-2);
+          & > span {
+            font-size: 12px;
+            color: var(--color-neutral-3);
+          }
+        }
+        .password-change {
+          display: flex;
+          gap: 8px;
+        }
+      }
+    }
+    .save-icon {
+      margin-top: 16px;
+      padding: 0 40px;
+    }
+  }
+}
+.personal-profile {
+  div.personal-preferences {
+    margin-top: 16px;
+    padding-bottom: 8px;
+    .segmented {
+      width: 291px;
+      height: 40px;
+      margin-bottom: 8px;
+      padding: 4px;
+      border-radius: 12px;
+      background-color: #f5f7fa;
+      .item {
+        display: inline-block;
+        height: 32px;
+        line-height: 32px;
+        font-weight: 700;
+        font-size: 16px;
+        text-align: center;
+        border-radius: 6px;
+        color: var(--color-neutral-2);
+        cursor: pointer;
+        &.is-active {
+          background-color: var(--color-white);
+          color: var(--color-neutral-1);
+        }
+      }
+    }
+    .date-format,
+    .numbers-format {
+      padding: 0px 16px 32px;
+      background-color: #f5f7fa;
+      border-radius: 12px;
+      .title {
+        height: 40px;
+        font-size: 14px;
+        font-weight: 700;
+        line-height: 40px;
+      }
+      .el-col {
+        margin-bottom: 8px;
+        &:last-child {
+          margin-bottom: 0;
+          margin-top: 32px;
+        }
+      }
+    }
+  }
+}
+</style>

+ 86 - 0
src/views/SystemMessage/src/components/SystemMessageDetail.vue

@@ -0,0 +1,86 @@
+<script setup lang="ts">
+import EventCard from '@/components/NotificationMessageCard/src/components/EventCard.vue'
+
+const notificationData: any = {
+  title: 'Milestone Update Daily Summary (Jan 10, 2025)',
+  numericRecords: 3,
+  notificationList: [
+    {
+      isRead: true,
+      mode: 'Ocean Freight',
+      no: 'HBOL: SHJN2301234',
+      tag: 'Booking Confirmed',
+      location: 'Hong Kong',
+      time: 'Jan 10, 2025 14:30 UTC+8'
+    },
+    {
+      isRead: false,
+      mode: 'Air Freight',
+      no: 'HBOL: SHJN2301234',
+      tag: 'Booking Confirmed',
+      location: 'Hong Kong',
+      time: 'Jan 10, 2025 14:30 UTC+8'
+    },
+    {
+      isRead: false,
+      mode: 'Air Freight',
+      no: 'HBOL: SHJN2301234',
+      tag: 'Booking Confirmed',
+      location: 'Hong Kong',
+      time: 'Jan 10, 2025 14:30 UTC+8',
+      previous: 'Previous: Departure from Shanghai (08:15 UTC+8)'
+    }
+  ]
+}
+</script>
+
+<template>
+  <div class="system-message-detail">
+    <div class="content">
+      <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>
+      <EventCard
+        v-for="(item, index) in notificationData.notificationList"
+        :key="index"
+        :data="item"
+      />
+    </div>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.system-message-detail {
+  padding: 16px;
+  .content {
+    margin: auto;
+    width: 800px;
+  }
+  .notification-card {
+    max-width: 800px;
+  }
+  .header {
+    display: flex;
+    align-items: center;
+    margin-bottom: 8px;
+    .status-icon {
+      width: 8px;
+      height: 8px;
+      border-radius: 50%;
+      background-color: var(--color-theme);
+      margin-right: 10px;
+    }
+    .title {
+      font-weight: 700;
+      font-size: 24px;
+    }
+  }
+  .total-tips {
+    height: 40px;
+    line-height: 40px;
+    font-size: 12px;
+  }
+}
+</style>

+ 13 - 3
src/views/Tracking/src/components/PublicTracking/src/components/BasicInformation.vue

@@ -1,5 +1,6 @@
 <script setup lang="ts">
 import XEClipboard from 'xe-clipboard'
+import { formatNumber } from '@/utils/tools'
 
 const props = defineProps({
   data: Object
@@ -77,6 +78,15 @@ const allData: any = ref({
     }
   ]
 })
+
+// 从开始位置截取字符串,并格式化数字(因为后端接口返回的内容带有字符串,所以需要截取)
+const substringFromStart = (str: string, start: number) => {
+  if (!str) {
+    return formatNumber(0, 3)
+  }
+  return formatNumber(Number(str.slice(0, start)), 3)
+}
+
 const convertData = (data: any) => {
   return {
     basicInformation: {
@@ -128,15 +138,15 @@ const convertData = (data: any) => {
       },
       {
         label: 'G. Weight',
-        content: data.packing['G. Weight'] || '--'
+        content: substringFromStart(data.packing['G. Weight'], -4) + ' KGS'
       },
       {
         label: 'Ch. Weight',
-        content: data.packing['Ch. Weight'] || '--'
+        content: substringFromStart(data.packing['Ch. Weight'], -4) + ' KGS'
       },
       {
         label: 'Volume',
-        content: data.packing.Volume || '--'
+        content: substringFromStart(data.packing['Volume'], -4) + ' CBM'
       }
     ]
   }

+ 1 - 1
src/views/Tracking/src/components/PublicTracking/src/components/PublicTrackingDetail.vue

@@ -1,7 +1,7 @@
 <script setup lang="ts">
 import BasicInformation from './BasicInformation.vue'
 import MilestonesTable from './MilestonesTable.vue'
-import { transportationMode } from '@/components/TransportationMode'
+import { transportationMode } from '@/components/transportationMode'
 import { useRoute } from 'vue-router'
 import { useOverflow } from '@/hooks/useOverflow'
 import { useThemeStore } from '@/stores/modules/theme'

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

@@ -11,7 +11,7 @@ import RoutesView from './components/RoutesView.vue'
 import AttachmentView from './components/AttachmentView.vue'
 import MapView from './components/MapView.vue'
 import { cloneDeep } from 'lodash'
-import { transportationMode } from '@/components/TransportationMode'
+import { transportationMode } from '@/components/transportationMode'
 import { useRoute } from 'vue-router'
 import { useOverflow } from '@/hooks/useOverflow'
 import { formatTimezone } from '@/utils/tools'
@@ -571,8 +571,3 @@ const SubscribeShipments = () => {
   }
 }
 </style>
-<style lang="scss">
-.is-show-tooltip {
-  // display: none;
-}
-</style>

+ 14 - 5
src/views/Tracking/src/components/TrackingDetail/src/components/BasicInformation.vue

@@ -1,6 +1,7 @@
 <script setup lang="ts">
 import XEClipboard from 'xe-clipboard'
-import AddReferenceDialog from './AddReferenceDialog.vue'
+// import AddReferenceDialog from './AddReferenceDialog.vue'
+import { formatNumber } from '@/utils/tools'
 
 const props = defineProps({
   data: Object
@@ -109,6 +110,14 @@ const allData: any = ref({
   ]
 })
 
+// 从开始位置截取字符串,并格式化数字(因为后端接口返回的内容带有字符串,所以需要截取)
+const substringFromStart = (str: string, start: number) => {
+  if (!str) {
+    return formatNumber(0, 3)
+  }
+  return formatNumber(Number(str.slice(0, start)), 3)
+}
+
 const convertData = (data: any) => {
   return {
     basicInformation: {
@@ -186,15 +195,15 @@ const convertData = (data: any) => {
       },
       {
         label: 'G. Weight',
-        content: data.packing['G. Weight'] || '--'
+        content: substringFromStart(data.packing['G. Weight'], -4) + ' KGS'
       },
       {
         label: 'Ch. Weight',
-        content: data.packing['Ch. Weight'] || '--'
+        content: substringFromStart(data.packing['Ch. Weight'], -4) + ' KGS'
       },
       {
         label: 'Volume',
-        content: data.packing.Volume || '--'
+        content: substringFromStart(data.packing['Volume'], -4) + ' CBM'
       }
     ],
     marksAndDescription: [
@@ -281,7 +290,7 @@ onBeforeUnmount(() => {
   window.removeEventListener('resize', checkTextOverflow)
 })
 
-const addReferenceRef = ref()
+// const addReferenceRef = ref()
 // const addReference = () => {
 //   addReferenceRef.value.openDialog()
 // }

+ 6 - 11
src/views/Tracking/src/components/TrackingDetail/src/components/ContainersView.vue

@@ -1,8 +1,8 @@
 <script setup lang="ts">
-import dayjs from 'dayjs'
 import { type VxeGridInstance, type VxeGridProps } from 'vxe-table'
-import { autoWidth } from '@/utils/table'
+// import { autoWidth } from '@/utils/table'
 import { useRowClickStyle } from '@/hooks/rowClickStyle'
+import { formatTimezone, formatNumber } from '@/utils/tools'
 
 const props = defineProps({
   data: Object
@@ -36,20 +36,15 @@ const handleColumns = (columns: any) => {
     }
 
     // 格式化
-    if (item.formatter === 'date') {
+    if (item.formatter === 'date' || item.formatter === 'dateTime') {
       curColumn = {
         ...curColumn,
-        sortBy: ({ row, column }: any) => {
-          return dayjs(row[column.field]).unix()
-        },
-        formatter: ({ cellValue }: any) =>
-          cellValue ? dayjs(cellValue).format('MMM-DD-YYYY ') : '--'
+        formatter: ({ cellValue }: any) => formatTimezone(cellValue)
       }
-    } else if (item.formatter === 'dateTime') {
+    } else if (item.formatter === 'number') {
       curColumn = {
         ...curColumn,
-        formatter: ({ cellValue }: any) =>
-          cellValue ? dayjs(cellValue).format('MMM-DD-YYYY HH:mm:ss') : '--'
+        formatter: ({ cellValue }: any) => formatNumber(Number(cellValue), item?.digits)
       }
     }
     return curColumn

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

@@ -1,8 +1,8 @@
 <script setup lang="ts">
-import dayjs from 'dayjs'
 import '@wangeditor/editor/dist/css/style.css'
 import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
 import { i18nChangeLanguage, DomEditor } from '@wangeditor/editor'
+import { formatTimezone } from '@/utils/tools'
 
 i18nChangeLanguage('en')
 
@@ -224,7 +224,7 @@ const sendEmail = () => {
             <div>{{ item.name?.slice(0, 1) }}</div>
           </div>
           <div class="name">{{ item.name }}</div>
-          <div class="date">{{ dayjs(item.creatTime).format('MM-DD-YYYY HH:mm:ss') }}</div>
+          <div class="date">{{ formatTimezone(item.creatTime) }}</div>
         </div>
         <div class="content">
           {{ item.content }}

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

@@ -15,7 +15,7 @@ import OriginIcon from '../images/originIcon.png'
 import TransferIcon from '../images/transferIcon.png'
 import * as turf from '@turf/turf'
 import { useThemeStore } from '@/stores/modules/theme'
-import { transportationMode } from '@/components/TransportationMode'
+import { transportationMode } from '@/components/transportationMode'
 
 const themeStore = useThemeStore()
 

+ 8 - 18
src/views/Tracking/src/components/TrackingDetail/src/components/RoutesView.vue

@@ -1,7 +1,7 @@
 <script setup lang="ts">
-import dayjs from 'dayjs'
-import { transportationMode } from '@/components/TransportationMode'
+import { transportationMode } from '@/components/transportationMode'
 import { useOverflow } from '@/hooks/useOverflow'
+import { formatTimezone } from '@/utils/tools'
 
 const props = defineProps({
   data: Object
@@ -50,16 +50,6 @@ watch(
   }
 )
 
-const formatDate = (date: string) => {
-  if (!date) {
-    return '--'
-  } else {
-    return date.length > 12
-      ? dayjs(date).format('MMM-DD-YYYY hh:mm A')
-      : dayjs(date).format('MMM-DD-YYYY')
-  }
-}
-
 const basicOriginRef = ref()
 const basicDestinationRef = ref()
 const detailOriginRef = ref()
@@ -117,11 +107,11 @@ const { isOverflow: isDetailDestinationOverflow } = useOverflow(detailDestinatio
           </div>
           <div class="etd border-right">
             <div class="title">ETD</div>
-            <div class="content">{{ formatDate(item.etd) }}</div>
+            <div class="content">{{ formatTimezone(item.etd) }}</div>
           </div>
           <div class="eta">
             <div class="title">ETA</div>
-            <div class="content">{{ formatDate(item.eta) }}</div>
+            <div class="content">{{ formatTimezone(item.eta) }}</div>
             <span
               :class="{ collapse: item.isCollapse }"
               class="font_family icon-icon_dropdown_b"
@@ -154,12 +144,12 @@ const { isOverflow: isDetailDestinationOverflow } = useOverflow(detailDestinatio
               <div class="etd">
                 <span class="font_family icon-icon_date_b"></span>
                 <span>ETD: </span>
-                <span class="value">{{ formatDate(item.etd) }}</span>
+                <span class="value">{{ formatTimezone(item.etd) }}</span>
               </div>
               <div class="atd">
                 <span class="font_family icon-icon_date_b"></span>
                 <span>ATD: </span>
-                <span class="value">{{ formatDate(item.atd) }}</span>
+                <span class="value">{{ formatTimezone(item.atd) }}</span>
               </div>
             </div>
             <div class="destination">
@@ -179,12 +169,12 @@ const { isOverflow: isDetailDestinationOverflow } = useOverflow(detailDestinatio
               <div class="eta">
                 <span class="font_family icon-icon_date_b"></span>
                 <span>ETA: </span>
-                <span class="value">{{ formatDate(item.eta) }}</span>
+                <span class="value">{{ formatTimezone(item.eta) }}</span>
               </div>
               <div class="ata">
                 <span class="font_family icon-icon_date_b"></span>
                 <span>ATA: </span>
-                <span class="value">{{ formatDate(item.ata) }}</span>
+                <span class="value">{{ formatTimezone(item.ata) }}</span>
               </div>
             </div>
           </div>

+ 6 - 7
src/views/Tracking/src/components/TrackingTable/src/TrackingTable.vue

@@ -5,10 +5,11 @@ import { useRowClickStyle } from '@/hooks/rowClickStyle'
 import dayjs from 'dayjs'
 import { useRouter } from 'vue-router'
 import { ref, onMounted } from 'vue'
-import { transportationMode } from '@/components/TransportationMode'
+import { transportationMode } from '@/components/transportationMode'
 import { useLoadingState } from '@/stores/modules/loadingState'
 import { useThemeStore } from '@/stores/modules/theme'
 import { useVisitedRowState } from '@/stores/modules/visitedRow'
+import { formatTimezone, formatNumber } from '@/utils/tools'
 
 const visitedRowState = useVisitedRowState()
 const themeStore = useThemeStore()
@@ -55,17 +56,15 @@ const handleColumns = (columns: any, status?: string) => {
       }
     }
     // 格式化
-    if (item.formatter === 'date') {
+    if (item.formatter === 'date' || item.formatter === 'dateTime') {
       curColumn = {
         ...curColumn,
-        formatter: ({ cellValue }: any) =>
-          cellValue ? dayjs(cellValue).format('MMM-DD-YYYY ') : '--'
+        formatter: ({ cellValue }: any) => formatTimezone(cellValue)
       }
-    } else if (item.formatter === 'dateTime') {
+    } else if (item.formatter === 'number') {
       curColumn = {
         ...curColumn,
-        formatter: ({ cellValue }: any) =>
-          cellValue ? dayjs(cellValue).format('MMM-DD-YYYY HH:mm:ss') : '--'
+        formatter: ({ cellValue }: any) => formatNumber(Number(cellValue), item?.digits)
       }
     }
     return curColumn

+ 11 - 13
src/views/Tracking/src/components/TrackingTable/src/components/VGMView.vue

@@ -1,8 +1,8 @@
 <script setup lang="ts">
-import dayjs from 'dayjs'
 import { useRoute, useRouter } from 'vue-router'
-import { autoWidth } from '@/utils/table'
+// import { autoWidth } from '@/utils/table'
 import { type VxeGridInstance, type VxeGridProps } from 'vxe-table'
+import { formatTimezone } from '@/utils/tools'
 
 const route = useRoute()
 const router = useRouter()
@@ -144,8 +144,7 @@ const handleColumns = (columns: any) => {
     if (item.edit_type === 'dateTime') {
       curColumn = {
         ...curColumn,
-        formatter: ({ cellValue }: any) =>
-          cellValue ? dayjs(cellValue).format('MMM-DD-YYYY HH:mm:ss') : '--'
+        formatter: ({ cellValue }: any) => formatTimezone(cellValue)
       }
     }
     return curColumn
@@ -181,11 +180,11 @@ const convertData = (data: any) => {
       },
       {
         label: 'ETD',
-        value: formatTime(data.ETD)
+        value: formatTimezone(data.ETD)
       },
       {
         label: 'ETA ',
-        value: formatTime(data.ETA)
+        value: formatTimezone(data.ETA)
       },
       {
         label: 'Last Updated User',
@@ -193,7 +192,7 @@ const convertData = (data: any) => {
       },
       {
         label: 'Last Updated Time',
-        value: formatTime(data?.['Last updated Time'])
+        value: formatTimezone(data?.['Last updated Time'])
       }
     ],
     formData: {
@@ -285,6 +284,10 @@ const isVerification = (value) => {
     }
   }
 }
+const formatRowTime = (time: any) => {
+  const result = formatTimezone(time)
+  return result === '--' ? '' : result
+}
 const handleSave = () => {
   generalInfo.value.formData.is_send && verificationData()
   if (
@@ -319,9 +322,7 @@ const handleSave = () => {
       if (item === '_X_ROW_KEY') return
       if (item === 'vgm_date' || item === 'vgm_time') {
         Object.assign(tableInfo, {
-          [item]: tableRowData.map((row) =>
-            row[item] ? dayjs(row[item]).format('YYYY-MM-DD HH:mm:ss') : ''
-          )
+          [item]: tableRowData.map((row) => formatRowTime(row[item]))
         })
         return
       }
@@ -348,9 +349,6 @@ const handleSave = () => {
     })
 }
 
-const formatTime = (time: string) => {
-  return time ? dayjs(time).format('MMM/DD/YYYY') : '--'
-}
 const stopScroll = (evt) => {
   evt = evt || window.event
   if (evt.preventDefault) {

Деякі файли не було показано, через те що забагато файлів було змінено