Jelajahi Sumber

Merge branch 'dev_zyh' of United_Software/k_online_ui into dev

Jack Zhou 1 tahun lalu
induk
melakukan
ad127e2f93
38 mengubah file dengan 907 tambahan dan 290 penghapusan
  1. 1 0
      .gitignore
  2. 1 0
      package.json
  3. 1 2
      src/App.vue
  4. 3 1
      src/api/index.ts
  5. 63 0
      src/api/module/login.ts
  6. 5 0
      src/auto-imports.d.ts
  7. 118 78
      src/components/ContainerStatus/src/ContainerStatus.vue
  8. 40 11
      src/components/CustomizeColumns/src/CustomizeColumns.vue
  9. 1 1
      src/components/ShipmentStatus/src/ShipmentStatus.vue
  10. 1 1
      src/components/VBox/src/VBox.vue
  11. 31 6
      src/components/VBreadcrumd/src/VBreadcrumd.vue
  12. 6 3
      src/components/VLoading/src/VLoading.vue
  13. 6 0
      src/components/transportationMode.ts
  14. 28 11
      src/router/index.ts
  15. 0 28
      src/stores/modules/parentPath.ts
  16. 67 3
      src/styles/elementui.scss
  17. 17 0
      src/styles/vxeTable.scss
  18. 11 3
      src/utils/axios.ts
  19. 13 2
      src/views/Booking/src/components/BookingDetail/src/BookingDetail.vue
  20. 5 2
      src/views/Booking/src/components/BookingDetail/src/components/BasicInformation.vue
  21. 0 14
      src/views/Booking/src/components/BookingDetail/src/components/EmailView.vue
  22. 18 12
      src/views/Booking/src/components/BookingTable/src/BookingTable.vue
  23. 3 3
      src/views/Layout/src/LayoutView.vue
  24. 11 3
      src/views/Layout/src/components/Header/HeaderView.vue
  25. 115 34
      src/views/Layout/src/components/Menu/MenuView.vue
  26. 79 0
      src/views/Login/src/components/LoginCard.vue
  27. 125 25
      src/views/Login/src/loginView.vue
  28. 8 1
      src/views/Tracking/src/components/PublicTracking/src/PublicTrackingSearch.vue
  29. 0 13
      src/views/Tracking/src/components/PublicTracking/src/components/PublicTrackingDetail.vue
  30. 24 5
      src/views/Tracking/src/components/TrackingDetail/src/TrackingDetail.vue
  31. 1 1
      src/views/Tracking/src/components/TrackingDetail/src/components/AddReferenceDialog.vue
  32. 0 2
      src/views/Tracking/src/components/TrackingDetail/src/components/AttachmentView.vue
  33. 1 1
      src/views/Tracking/src/components/TrackingDetail/src/components/BasicInformation.vue
  34. 0 13
      src/views/Tracking/src/components/TrackingDetail/src/components/EmailDrawer.vue
  35. 84 0
      src/views/Tracking/src/components/TrackingDetail/src/components/MapView.vue
  36. 9 5
      src/views/Tracking/src/components/TrackingDetail/src/components/TransportStep.vue
  37. TEMPAT SAMPAH
      src/views/Tracking/src/components/TrackingDetail/src/images/location.png
  38. 11 6
      src/views/Tracking/src/components/TrackingTable/src/TrackingTable.vue

+ 1 - 0
.gitignore

@@ -16,6 +16,7 @@ coverage
 components.d.ts
 package-lock.json
 pnpm-lock.yaml
+auto-imports.d.ts
 
 /cypress/videos/
 /cypress/screenshots/

+ 1 - 0
package.json

@@ -40,6 +40,7 @@
   "devDependencies": {
     "@rushstack/eslint-patch": "^1.8.0",
     "@tsconfig/node20": "^20.1.4",
+    "@types/leaflet": "^1.9.12",
     "@types/lodash": "^4.17.7",
     "@types/node": "^20.14.5",
     "@vitejs/plugin-vue": "^5.0.5",

+ 1 - 2
src/App.vue

@@ -1,6 +1,5 @@
 <script setup lang="ts">
-import { RouterLink, RouterView } from 'vue-router'
-import HelloWorld from './components/HelloWorld.vue'
+import { RouterView } from 'vue-router'
 </script>
 
 <template>

+ 3 - 1
src/api/index.ts

@@ -1,6 +1,7 @@
 import * as booking from './module/booking'
 import * as tracking from './module/tracking'
 import * as common from './module/common'
+import * as login from './module/login'
 /**
  * api 对象接口定义
  */
@@ -15,7 +16,8 @@ function generateApiMap(maps: any) {
 const apis = generateApiMap({
   ...booking,
   ...tracking,
-  ...common
+  ...common,
+  ...login
 })
 export default {
   ...apis // 取出所有可遍历属性赋值在新的对象上

+ 63 - 0
src/api/module/login.ts

@@ -0,0 +1,63 @@
+import HttpAxios from '@/utils/axios'
+
+const baseUrl = 'http://localhost/api/Customer_Service_Online/login.php'
+
+/**
+ * 获取验证码
+ */
+export const getVerifcationCode = (params: any, config: any) => {
+  return HttpAxios.get(
+    `${baseUrl}`,
+    {
+      action: 'login',
+      operate: 'verifcation_code',
+      ...params
+    },
+    config
+  )
+}
+
+/**
+ * 验证用户名是否存在
+ */
+export const isUserNameExit = (params: any, config: any) => {
+  return HttpAxios.post(
+    `${baseUrl}`,
+    {
+      action: 'login',
+      operate: 'check_uname',
+      ...params
+    },
+    config
+  )
+}
+
+/**
+ * 登录
+ */
+export const login = (params: any, config: any) => {
+  return HttpAxios.post(
+    `${baseUrl}`,
+    {
+      action: 'login',
+      operate: 'do_login',
+      ...params
+    },
+    config
+  )
+}
+
+/**
+ * 忘记密码
+ */
+export const forgotPassword = (params: any, config: any) => {
+  return HttpAxios.post(
+    `${baseUrl}`,
+    {
+      action: 'login',
+      operate: 'forgot_password',
+      ...params
+    },
+    config
+  )
+}

+ 5 - 0
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']
@@ -38,6 +39,7 @@ declare global {
   const onServerPrefetch: typeof import('vue')['onServerPrefetch']
   const onUnmounted: typeof import('vue')['onUnmounted']
   const onUpdated: typeof import('vue')['onUpdated']
+  const onWatcherCleanup: typeof import('vue')['onWatcherCleanup']
   const provide: typeof import('vue')['provide']
   const reactive: typeof import('vue')['reactive']
   const readonly: typeof import('vue')['readonly']
@@ -55,7 +57,10 @@ declare global {
   const useAttrs: typeof import('vue')['useAttrs']
   const useCssModule: typeof import('vue')['useCssModule']
   const useCssVars: typeof import('vue')['useCssVars']
+  const useId: typeof import('vue')['useId']
+  const useModel: typeof import('vue')['useModel']
   const useSlots: typeof import('vue')['useSlots']
+  const useTemplateRef: typeof import('vue')['useTemplateRef']
   const watch: typeof import('vue')['watch']
   const watchEffect: typeof import('vue')['watchEffect']
   const watchPostEffect: typeof import('vue')['watchPostEffect']

+ 118 - 78
src/components/ContainerStatus/src/ContainerStatus.vue

@@ -5,38 +5,14 @@ const props = defineProps({
   data: Object
 })
 
-const containerStatusData: any = ref([
-  {
-    title: 'Empty Equipment Dispatched',
-    date: 'May-31-2024 10:03(GMT+02)',
-    country: 'CN,SHK'
-  },
-  {
-    title: 'In-Gate',
-    date: 'May-31-2024 10:03(GMT+02)',
-    country: 'CN,SHK'
-  },
-  {
-    title: 'Loaded On Vessel',
-    date: 'Jun-05-2024 10:03(GMT+02)',
-    country: 'CN,SHK'
-  },
-  {
-    title: 'Vessel Departure',
-    date: 'Jun-06-2024 10:03(GMT+02)',
-    country: 'CN,SHK'
-  },
-  {
-    title: 'Vessel Arrival',
-    date: 'Jun-07-2024 14:00(GMT+02)',
-    country: 'ES,BCN'
-  }
-])
+const activeNames = ref<string[]>([])
+
+const containerStatusData: any = ref([])
 watch(
   () => props.data,
   (newVal) => {
     if (newVal) {
-      containerStatusData.value = newVal[0].content
+      containerStatusData.value = newVal
     } else {
       containerStatusData.value = []
     }
@@ -54,72 +30,136 @@ const formatDate = (date: string) => {
 
 <template>
   <div class="container-status">
-    <div class="step-item" v-for="(item, index) in containerStatusData" :key="item.title">
-      <div class="step-data">
-        <div class="step-dot-icon"></div>
-        <div class="info">
-          <div class="left-info">
-            <div class="title">{{ item.title }}</div>
-            <div class="date">{{ formatDate(item.date) }}</div>
+    <el-collapse class="container" v-model="activeNames">
+      <el-collapse-item
+        :title="containers.label"
+        v-for="(containers, name) in containerStatusData"
+        :key="name"
+        :name="name"
+      >
+        <template #title>
+          <div class="title">
+            Container <span>{{ containers.label }}</span>
+          </div>
+        </template>
+        <div class="step-item" v-for="(item, index) in containers.content" :key="item.title">
+          <div class="step-data">
+            <div class="step-dot-icon"></div>
+            <div class="info">
+              <div class="left-info">
+                <div class="title">{{ item.title }}</div>
+                <div class="date">{{ formatDate(item.date) }}</div>
+              </div>
+              <div class="right-country">{{ item.country }}</div>
+            </div>
           </div>
-          <div class="right-country">{{ item.country }}</div>
+          <div class="line" v-if="index + 1 !== containers.content.length"></div>
         </div>
-      </div>
-      <div class="line" v-if="index + 1 !== containerStatusData.length"></div>
+      </el-collapse-item>
+    </el-collapse>
+    <div class="footer">
+      Tracking on carrier website:
+      <a href="http://www.rcjgroup.com/" target="_blank" class="link">http://www.rcjgroup.com/</a>
     </div>
   </div>
 </template>
 
 <style lang="scss" scoped>
 .container-status {
+  position: relative;
   width: 100%;
-  padding: 22px 0 22px;
+  height: 100%;
+  overflow: auto;
+  .container {
+    height: 394px;
+    padding-bottom: 8px;
+    overflow: auto;
+  }
+  .footer {
+    line-height: 38px;
+    color: #999;
+    font-size: 12px;
+    text-align: center;
+    border-top: 1px solid var(--color-border);
+    overflow: hidden;
+    .link {
+      text-decoration: none;
+      color: var(--color-theme);
+    }
+  }
 }
-
-.step-data {
-  display: flex;
-  gap: 8px;
-  height: 8px;
-  width: 100%;
-  .step-dot-icon {
-    height: 8px;
-    width: 8px;
-    background-color: var(--color-neutral-1);
-    border-radius: 50%;
+</style>
+<style lang="scss">
+.container-status {
+  .el-collapse {
+    height: auto;
+    border-bottom: 0;
+  }
+  .el-collapse-item__content {
+    padding: 30px 16px 38px;
+    border-top: 1px solid var(--color-border);
+  }
+  .el-collapse-item__header {
+    height: 40px;
+    padding-left: 16px;
+    padding-right: 8px;
+    line-height: 50px;
+    & > .title {
+      font-size: 16px;
+      span {
+        margin-left: 4px;
+        font-weight: 700;
+        color: var(--color-neutral-1);
+      }
+    }
   }
-  .info {
-    flex: 1;
+  .step-data {
     display: flex;
-    height: 52px;
-    margin-top: -22px;
-    background-color: var(--color-table-header-bg);
-    border: 1px solid var(--color-border);
-    border-radius: 6px;
-    color: var(--color-neutral-1);
-    .left-info {
+    gap: 8px;
+    height: 8px;
+    width: 100%;
+    .step-dot-icon {
+      height: 8px;
+      width: 8px;
+      background-color: var(--color-neutral-1);
+      border-radius: 50%;
+    }
+    .info {
       flex: 1;
-      padding: 8px 10px;
-      border-right: 1px solid var(--color-border);
-      .title {
-        font-size: 14px;
-        font-weight: 700;
+      display: flex;
+      height: 52px;
+      margin-top: -22px;
+      background-color: var(--color-table-header-bg);
+      border: 1px solid var(--color-border);
+      border-radius: 6px;
+      color: var(--color-neutral-1);
+      .left-info {
+        flex: 1;
+        display: flex;
+        flex-direction: column;
+        padding: 8px 10px;
+        border-right: 1px solid var(--color-border);
+        .title {
+          font-size: 14px;
+          line-height: 18px;
+          font-weight: 700;
+        }
+        .date {
+          font-size: 12px;
+        }
       }
-      .date {
-        margin-top: 4px;
-        font-size: 12px;
+      .right-country {
+        padding: 0 24px;
+        line-height: 52px;
+        font-weight: 700;
       }
     }
-    .right-country {
-      padding: 0 24px;
-      line-height: 52px;
-      font-weight: 700;
-    }
   }
-}
-.line {
-  height: 60px;
-  width: 0;
-  margin-left: 3px;
-  border-left: 1px solid var(--color-neutral-1);
+  .line {
+    height: 60px;
+    width: 0;
+    margin-left: 3px;
+    border-left: 1px solid var(--color-neutral-1);
+  }
 }
 </style>

+ 40 - 11
src/components/CustomizeColumns/src/CustomizeColumns.vue

@@ -105,13 +105,21 @@ const scrollToItem = (itemId: string) => {
 }
 
 // 系统首次加载时,会有引导操作
-let firstLoad = localStorage.getItem('firstLoadCustomizeColumns')
+let firstLoad = ref()
 const step1 = ref()
 const open1 = ref(false)
+const isShowStep1 = ref(false)
 const step2 = ref()
 const open2 = ref(false)
+const isShowStep2 = ref(false)
 const handleCloseTour = (stepStr: string) => {
-  stepStr === 'step1' ? (open1.value = false) : (open2.value = false)
+  if (stepStr === 'step1') {
+    isShowStep1.value = false
+    open1.value = false
+  } else {
+    isShowStep2.value = false
+    open2.value = false
+  }
   localStorage.setItem('firstLoadCustomizeColumns', 'true')
   // firstLoad = 'true'
 }
@@ -151,14 +159,22 @@ const getData = async (reset?: string) => {
 const params = ref()
 // rightDistance是右侧箭头消失所需要translateX的值
 const openDialog = async (paramsData: Object, rightDistance: number) => {
+  firstLoad.value = localStorage.getItem('firstLoadCustomizeColumns')
   params.value = paramsData
   dialogVisible.value = true
   await getData()
   rightArrowHideDistance.value = rightDistance
   nextTick(() => {
-    if (!firstLoad) {
+    if (!firstLoad.value) {
       open1.value = true
+      isShowStep1.value = true
       open2.value = true
+      isShowStep2.value = true
+      // 五秒后关闭引导
+      setTimeout(() => {
+        handleCloseTour('step1')
+        handleCloseTour('step2')
+      }, 5000)
     }
   })
 }
@@ -383,7 +399,7 @@ defineExpose({
                     <span class="title">{{ item.label }}</span>
                     <span
                       ref="step1"
-                      v-if="hoverAllIcon === item.field || (index === 0 && !firstLoad)"
+                      v-if="hoverAllIcon === item.field || (index === 0 && isShowStep1)"
                       class="font_family icon-icon_add_b move-icon"
                       @click="handleAddSelect(item)"
                     ></span>
@@ -419,18 +435,18 @@ defineExpose({
               ></span>
               <span class="title">{{ item.label }}</span>
               <span
-                v-if="hoverSelectIcon === item.field || (index === 0 && !firstLoad)"
+                v-if="hoverSelectIcon === item.field || (index === 0 && isShowStep2)"
                 class="font_family icon-icon_moveup_b move-icon"
                 @click="handleMoveUpSelect(item)"
               ></span>
               <span
-                v-if="hoverSelectIcon === item.field || (index === 0 && !firstLoad)"
+                v-if="hoverSelectIcon === item.field || (index === 0 && isShowStep2)"
                 class="font_family icon-icon_movedown_b move-icon"
                 @click="handleMoveDownSelect(item)"
               ></span>
               <span
                 ref="step2"
-                v-if="hoverSelectIcon === item.field || (index === 0 && !firstLoad)"
+                v-if="hoverSelectIcon === item.field || (index === 0 && isShowStep2)"
                 class="font_family icon-icon_reduce_b move-icon"
                 @click="handleDeleteSelect(item)"
               ></span>
@@ -440,16 +456,29 @@ defineExpose({
       </div>
     </div>
     <template #footer>
-      <el-button type="default" @click="dialogVisible = false">Cancel</el-button>
-      <el-button type="default" @click="handleReset">Reset to default</el-button>
-      <el-button class="el-button--dark" @click="handleApply"> Apply </el-button>
+      <el-button
+        type="default"
+        style="height: 40px; padding: 8px 40px"
+        @click="dialogVisible = false"
+        >Cancel</el-button
+      >
+      <el-button type="default" style="height: 40px; padding: 8px 20px" @click="handleReset"
+        >Reset to default</el-button
+      >
+      <el-button
+        class="el-button--dark"
+        style="height: 40px; padding: 8px 40px"
+        @click="handleApply"
+      >
+        Apply
+      </el-button>
     </template>
     <el-tour
       :target-area-clickable="false"
       class="step1-tour"
       v-model="open1"
-      type="primary"
       :mask="false"
+      type="primary"
       v-if="step1?.[0]"
     >
       <el-tour-step :show-close="false" :target="step1?.[0]">

+ 1 - 1
src/components/ShipmentStatus/src/ShipmentStatus.vue

@@ -81,7 +81,7 @@ const formatDate = (date: string) => {
 // 单式样式
 .simplex-content {
   width: 100%;
-  padding: 8px 0 9px;
+  padding: 24px 8px 9px 16px;
 }
 .detail-step-item {
   & > .data {

+ 1 - 1
src/components/VBox/src/VBox.vue

@@ -74,7 +74,7 @@ const vBoxPopoverRef = ref()
             </div>
           </div>
           <template #reference v-if="props.isDraggable">
-            <el-button type="text" class="sort handle-draggable">
+            <el-button class="sort handle-draggable el-button--text">
               <span class="font_family icon-icon_dragsort__b" style="font-size: 16px"></span>
             </el-button>
           </template>

+ 31 - 6
src/components/VBreadcrumd/src/VBreadcrumd.vue

@@ -1,19 +1,44 @@
 <script setup lang="ts">
-import { useParentPathStore } from '@/stores/modules/parentPath'
-import { useRouter } from 'vue-router'
+import { useRouter, useRoute } from 'vue-router'
 
 const router = useRouter()
-const parentPathStore = useParentPathStore()
+const route = useRoute()
 
+const path = computed(() => {
+  const path = route.path
+  // 检查URL中是否包含'detail'
+  if (path.includes('/detail')) {
+    // 获取'detail'的位置
+    const detailIndex = path.indexOf('/detail')
+    // 从开始到'/detail'前一个'/'的位置的子串
+    const lastSlashIndex = path.lastIndexOf('/', detailIndex - 1)
+    // 如果找到了'/'
+    if (lastSlashIndex !== -1) {
+      // 获取上一段字符串
+      return path.substring(lastSlashIndex + 1, detailIndex)
+    }
+  }
+  // 如果没有找到或者不符合条件,则返回null
+  return null
+})
+
+const mapPathName = {
+  booking: 'Booking',
+  tracking: 'Tracking',
+  'public-tracking': 'Public Tracking'
+}
 const handleGoBack = () => {
-  router.push({ path: parentPathStore.parentPathInfo?.fullPath })
+  router.push({ path: '/' + path.value })
 }
+const pathName = computed(() => {
+  return mapPathName[path.value]
+})
 </script>
 
 <template>
-  <div class="v-breadcrumd" v-if="parentPathStore.parentPathInfo?.name">
+  <div class="v-breadcrumd" v-if="path">
     <span class="font_family icon-icon_back_b" @click="handleGoBack"></span>
-    <span style="color: var(--color-neutral-3)">{{ parentPathStore.parentPathInfo?.name }}</span>
+    <span style="color: var(--color-neutral-3)">{{ pathName }}</span>
     <span class="interval">|</span>
     <span style="font-weight: 700">Detail</span>
   </div>

+ 6 - 3
src/components/VLoading/src/VLoading.vue

@@ -8,7 +8,7 @@
 interface internalProps {
   target?: string
   loading: boolean
-  isLoadingBackground: boolean
+  isLoadingBackground?: boolean
 }
 
 const props = withDefaults(defineProps<internalProps>(), {
@@ -18,8 +18,11 @@ const props = withDefaults(defineProps<internalProps>(), {
 </script>
 <template>
   <Teleport :to="props.target">
-    <div v-if="props.loading" class="v-loading-mask"
-      :style="{ background: props.isLoadingBackground ? 'none' : '#2B2F36' }">
+    <div
+      v-if="props.loading"
+      class="v-loading-mask"
+      :style="{ background: props.isLoadingBackground ? 'none' : '#2B2F36' }"
+    >
       <div class="v-loading-spinner">
         <div>
           <img class="circular" src="./images/icon_loading.png" alt="" />

+ 6 - 0
src/components/transportationMode.ts

@@ -0,0 +1,6 @@
+export const transportationMode: any = {
+  'Ocean Freight': 'icon_ocean_b',
+  'Air Freight': 'icon_airplane_b',
+  'Road Freight': 'icon_truck_b',
+  'Rail Freight': 'icon_railway_b'
+}

+ 28 - 11
src/router/index.ts

@@ -1,5 +1,4 @@
 import { createRouter, createWebHistory } from 'vue-router'
-import { useParentPathStore } from '@/stores/modules/parentPath'
 
 const router = createRouter({
   history: createWebHistory(import.meta.env.BASE_URL),
@@ -25,7 +24,7 @@ const router = createRouter({
           name: 'Booking Detail',
           component: () => import('../views/Booking/src/components/BookingDetail'),
           meta: {
-            parentPath: '/booking'
+            activeMenu: '/booking'
           }
         },
         {
@@ -36,12 +35,18 @@ const router = createRouter({
         {
           path: '/tracking/detail',
           name: 'Tracking Detail',
-          component: () => import('../views/Tracking/src/components/TrackingDetail')
+          component: () => import('../views/Tracking/src/components/TrackingDetail'),
+          meta: {
+            activeMenu: '/tracking'
+          }
         },
         {
           path: '/public-tracking',
           name: 'Public Tracking',
-          component: () => import('../views/Tracking/src/components/PublicTracking')
+          component: () => import('../views/Tracking/src/components/PublicTracking'),
+          meta: {
+            activeMenu: '/tracking'
+          }
         },
         {
           path: '/public-tracking/detail',
@@ -49,12 +54,18 @@ const router = createRouter({
           component: () =>
             import(
               '../views/Tracking/src/components/PublicTracking/src/components/PublicTrackingDetail.vue'
-            )
+            ),
+          meta: {
+            activeMenu: '/tracking'
+          }
         },
         {
           path: '/login',
           name: 'Login',
-          component: () => import('../views/Login')
+          component: () => import('../views/Login'),
+          meta: {
+            activeMenu: '/tracking'
+          }
         },
         {
           path: '/Operationlog',
@@ -68,11 +79,17 @@ const router = createRouter({
 
 // * 路由拦截 beforeEach
 router.beforeEach(async (to, from, next) => {
-  const parentPathStore = useParentPathStore()
-  if (to.path.includes('/detail')) {
-    parentPathStore.setParentPath(from)
-  } else {
-    parentPathStore.clearParentPath()
+  // 未登录白名单
+  const whiteList = ['/login', '/public-tracking', '/public-tracking/detail']
+  // 判断是否登录
+  if (!whiteList.includes(to.path) && !localStorage.getItem('token')) {
+    if (whiteList.includes(from.path)) {
+      ElMessage.warning('Please log in to use this feature.')
+      next(false)
+      return
+    } else {
+      next('/public-tracking')
+    }
   }
   next()
 })

+ 0 - 28
src/stores/modules/parentPath.ts

@@ -1,28 +0,0 @@
-import { defineStore } from 'pinia'
-interface ParentPath {
-  parentPathInfo: {
-    fullPath?: string
-    name?: string
-  }
-}
-
-export const useParentPathStore = defineStore('parentPath', {
-  state: (): ParentPath => ({
-    parentPathInfo: JSON.parse(localStorage.getItem('parentPathInfo') as string) || {}
-  }),
-  getters: {},
-  actions: {
-    setParentPath(route: any) {
-      const pathInfo = {
-        name: route.name,
-        fullPath: route.fullPath
-      }
-      localStorage.setItem('parentPath', JSON.stringify(pathInfo))
-      this.parentPathInfo = pathInfo
-    },
-    clearParentPath() {
-      localStorage.removeItem('parentPath')
-      this.parentPathInfo = {}
-    }
-  }
-})

+ 67 - 3
src/styles/elementui.scss

@@ -239,6 +239,67 @@ label.el-radio {
   }
 }
 
+// message
+div.el-message {
+  display: flex;
+  justify-content: center;
+  width: 800px;
+}
+div.el-message--error {
+  --el-message-bg-color: #faebec;
+  --el-message-border-color: #faebec;
+  --el-message-text-color: var(--color-danger);
+}
+div.el-message--success {
+  --el-message-bg-color: #e5f6f1;
+  --el-message-border-color: #e5f6f1;
+  --el-message-text-color: var(--color-success);
+}
+
+div.el-message--warning {
+  --el-message-bg-color: #fef5eb;
+  --el-message-border-color: #fef5eb;
+  --el-message-text-color: #f19d38;
+}
+
+// message box
+div.el-message-box {
+  --el-messagebox-width: 640px;
+  --el-messagebox-border-radius: 12px;
+  padding: 0;
+  .el-message-box__header {
+    height: 56px;
+    padding: 16px;
+    background-color: var(--color-table-header-bg);
+    & > span {
+      font-weight: 700;
+      font-size: var(--font-size-4);
+    }
+  }
+  .el-message-box__container {
+    padding: 24px 16px;
+    gap: 6px;
+  }
+  .el-message-box__headerbtn {
+    height: 56px;
+  }
+  .el-message-box__status {
+    font-size: 16px;
+  }
+  .el-message-box__title {
+    font-weight: 700;
+    font-size: var(--font-size-4);
+  }
+  .el-message-box__message {
+    font-size: var(--font-size-3);
+  }
+  .el-message-box__btns {
+    padding: 8px;
+    padding-right: 16px;
+    border-top: 1px solid var(--color-border);
+  }
+}
+
 // drawer抽屉
 div.el-drawer {
   .el-drawer__header {
@@ -512,10 +573,13 @@ div .el-tabs__active-bar {
   background-color: var(--color-accent-2);
 }
 
-div .el-radio-button.is-active .el-radio-button__original-radio:not(:disabled)+.el-radio-button__inner {
+div
+  .el-radio-button.is-active
+  .el-radio-button__original-radio:not(:disabled)
+  + .el-radio-button__inner {
   background-color: var(--color-theme);
   border-color: var(--color-theme);
-  box-shadow:none
+  box-shadow: none;
 }
 div .el-radio-button__inner:hover {
   color: var(--color-theme);
@@ -523,4 +587,4 @@ div .el-radio-button__inner:hover {
 div .el-space {
   flex-wrap: wrap;
   margin: 3px 0 0 0;
-}
+}

+ 17 - 0
src/styles/vxeTable.scss

@@ -86,3 +86,20 @@ div.vxe-table--render-default .vxe-cell--checkbox:not(.is--disabled):hover .vxe-
 .vxe-table--tooltip-wrapper.is--active {
   z-index: 9999 !important;
 }
+
+div.w-e-bar svg {
+  height: 16px;
+  width: 16px;
+}
+
+[data-menu-key='group-image'] {
+  & + .w-e-bar-item-menus-container {
+    .w-e-bar-item:first-child {
+      display: none;
+    }
+  }
+}
+
+.w-e-bar.w-e-hover-bar.w-e-bar-show {
+  display: none;
+}

+ 11 - 3
src/utils/axios.ts

@@ -1,4 +1,5 @@
 import axios, { type AxiosInstance, type AxiosRequestConfig, type AxiosResponse } from 'axios'
+import router from '@/router'
 import { ElMessage, ElMessageBox } from 'element-plus'
 // import {
 //   showFullScreenLoading,
@@ -46,14 +47,21 @@ class HttpAxios {
     const _config = { timeout: this.timeout }
     return { ...config, ..._config }
   }
-
+  /**
+   * 返回拦截
+   * @param response
+   * @returns
+   */
   _responseInterceptors = (response: AxiosResponse) => {
     if (response.status === 200) {
-      if (response.data.code !== 200) {
+      if (response.data.code === 401 || response.data.code === 403) {
+        router.push('/login')
+        ElMessage.warning('Please log in to use this feature.')
+      } else if (response.data.code !== 200 && response.data.code !== 400) {
         ElMessageBox.alert(
           response.data?.data?.msg || 'The request failed. Please try again later',
           'Prompt',
-          { confirmButtonText: 'OK' }
+          { confirmButtonText: 'OK', confirmButtonClass: 'el-button--dark' }
         )
       }
       return response.data

+ 13 - 2
src/views/Booking/src/components/BookingDetail/src/BookingDetail.vue

@@ -5,6 +5,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'
 
 // 可拖拽模块的列表
 const boxList = ref([
@@ -61,7 +62,9 @@ const handleDraggable = (type: string, id: number) => {
 }
 
 const allData = ref()
+const loading = ref(false)
 const getData = () => {
+  loading.value = true
   $api
     .getBookingDetail({
       status: 'Confirmed',
@@ -74,6 +77,9 @@ const getData = () => {
         allData.value = res.data
       }
     })
+    .finally(() => {
+      loading.value = false
+    })
 }
 getData()
 
@@ -83,10 +89,14 @@ const formatTime = (time: string) => {
 </script>
 
 <template>
-  <div class="booking-detail">
+  <div class="booking-detail" v-vloading="loading">
     <div class="header">
       <div class="detail-status">
-        <span class="font_family icon-icon_ocean_b" style="font-size: 64px"></span>
+        <span
+          class="font_family"
+          :class="[`icon-${transportationMode?.[allData?.transportInfo?.mode]}`]"
+          style="font-size: 64px"
+        ></span>
         <div class="no">Booking No. {{ allData?.transportInfo?.['bookingNo.'] }}</div>
         <VTag large type="Confirmed">{{ allData?.transportInfo?.status }}</VTag>
       </div>
@@ -205,6 +215,7 @@ const formatTime = (time: string) => {
       position: relative;
       display: flex;
       align-items: center;
+      height: 64px;
       padding: 0 16px;
       border-bottom: 1px solid var(--color-border);
       .no {

+ 5 - 2
src/views/Booking/src/components/BookingDetail/src/components/BasicInformation.vue

@@ -262,7 +262,10 @@ watch(
 
 // 跳转到shipment页面
 const handLink = (id: string) => {
-  router.push({ path: '/tracking', query: { id } })
+  router.push({
+    path: '/tracking',
+    query: { a: props?.data?.__serial_no, _schemas: props?.data?._schemas }
+  })
 }
 
 const handleCopy = (data: any) => {
@@ -299,7 +302,7 @@ defineExpose({
           <!-- <el-button
             style="font-size: 12px"
             v-if="item.label === 'Ref No.'"
-            type="text"
+            class="el-button-text"
             @click="addReference"
           >
             <span class="font_family icon-icon_add_b"></span>

+ 0 - 14
src/views/Booking/src/components/BookingDetail/src/components/EmailView.vue

@@ -322,17 +322,3 @@ const sendEmail = () => {
   }
 }
 </style>
-<style lang="scss">
-div.w-e-bar svg {
-  height: 16px;
-  width: 16px;
-}
-
-[data-menu-key='group-image'] {
-  & + .w-e-bar-item-menus-container {
-    .w-e-bar-item:first-child {
-      display: none;
-    }
-  }
-}
-</style>

+ 18 - 12
src/views/Booking/src/components/BookingTable/src/BookingTable.vue

@@ -1,9 +1,9 @@
 <script setup lang="ts">
+import { transportationMode } from '@/components/TransportationMode'
 import { type VxeGridInstance, type VxeGridProps } from 'vxe-table'
 import DownloadDialog from './components/DownloadDialog.vue'
 import { autoWidth } from '@/utils/table'
 import { useRowClickStyle } from '@/hooks/rowClickStyle'
-import { ref, onMounted, nextTick } from 'vue'
 import dayjs from 'dayjs'
 import { useRouter } from 'vue-router'
 
@@ -83,21 +83,22 @@ const getTableColumns = async (isInit: boolean) => {
 }
 
 const pageInfo = ref({ pageNo: 1, pageSize: 100, total: 0 })
+const querydata = ref()
 const TransportListItem = ref()
 const TagsList = ref()
 
 // 获取表格数据
-let filterdataobj: any = {}
 const getTableData = async (isInit: boolean, isPageChange?: boolean) => {
   const rc = isPageChange ? pageInfo.value.total : -1
   tableLoading.value = true
+  querydata.value = { cp: pageInfo.value.pageNo, ps: pageInfo.value.pageSize, rc: rc }
   await $api
     .getBookingTableData({
       cp: pageInfo.value.pageNo,
       ps: pageInfo.value.pageSize,
       rc,
       other_filed: '',
-      ...filterdataobj
+      _textSearch: ''
     })
     .then((res: any) => {
       if (res.code === 200) {
@@ -124,20 +125,18 @@ const getTableData = async (isInit: boolean, isPageChange?: boolean) => {
 const searchTableData = (data: any) => {
   tableLoading.value = true
   console.log(data)
-  filterdataobj = data
   $api
     .getBookingTableData({
       cp: pageInfo.value.pageNo,
       ps: pageInfo.value.pageSize,
-      rc: -1,
+      rc: pageInfo.value.total,
       other_filed: '',
       _textSearch: '',
-      ...data
+      filterTag: data.filterTag ? data.filterTag : ['All']
     })
     .then((res: any) => {
       if (res.code === 200) {
         bookingTable.value.data = res.data.searchData
-        pageInfo.value.total = Number(res.data.rc)
         tableLoading.value = false
       }
     })
@@ -226,6 +225,8 @@ const allTableRef = ref<VxeGridInstance>()
 const allTable = ref<VxeGridProps<any>>({
   columns: [],
   data: [],
+  showHeaderOverflow: true,
+  showOverflow: true,
   scrollY: { enabled: true, oSize: 5, gt: 2 },
   scrollX: { enabled: true, gt: 2 },
   exportConfig: {
@@ -299,7 +300,7 @@ const customizeColumns = async () => {
 const handleCellDblclick = ({ row }: any) => {
   router.push({
     path: '/booking/detail',
-    query: { id: row.booking_no }
+    query: { a: row.__serial_no, _schemas: row.__schemas }
   })
 }
 // 点击link字段是时
@@ -307,12 +308,12 @@ const handleLinkClick = (row: any, column: any) => {
   if (column.field === 'booking_no') {
     router.push({
       path: '/booking/detail',
-      query: { id: row.booking_no }
+      query: { a: row.__serial_no, _schemas: row.__schemas }
     })
   } else if (column.field === 'h_bol') {
     router.push({
       path: '/tracking/detail',
-      query: { id: row.h_bol }
+      query: { a: row.__serial_no, _schemas: row.__schemas }
     })
   }
 }
@@ -326,6 +327,7 @@ const handleCheckAllChange = ({ records }: any) => {
   selectedNumber.value = records.length
 }
 defineExpose({
+  querydata,
   searchTableData,
   TransportListItem,
   TagsList
@@ -372,9 +374,13 @@ defineExpose({
         </VEmpty>
       </template>
       <!-- Transportation Mode字段的插槽 -->
-      <template #mode>
+      <template #mode="{ row, column }">
         <div>
-          <span class="font_family icon-icon_ocean_b" style="font-size: 24px"></span>
+          <span
+            class="font_family"
+            :class="[`icon-${transportationMode?.[row[column.field]]}`]"
+            style="font-size: 24px"
+          ></span>
         </div>
       </template>
       <!-- Status字段的插槽 -->

+ 3 - 3
src/views/Layout/src/LayoutView.vue

@@ -69,11 +69,11 @@ const handleMenuCollapse = (val: boolean) => {
   .layout-content {
     padding: 0;
     & > .el-scrollbar {
-      :deep(.el-scrollbar__view) {
-        height: 100%;
-      }
       :deep(.el-scrollbar__wrap--hidden-default) {
         border-radius: 0;
+        & > .el-scrollbar__view {
+          height: 100%;
+        }
       }
     }
   }

+ 11 - 3
src/views/Layout/src/components/Header/HeaderView.vue

@@ -21,13 +21,20 @@ const handleChangePassword = () => {
 const logoutDialogRef = ref()
 
 const handleLogout = () => {
-  console.log('logout')
   logoutDialogRef.value.openDialog()
 }
 
 const handleLogin = () => {
   router.push('/login')
 }
+
+const test = () => {
+  if (localStorage.getItem('token')) {
+    localStorage.removeItem('token')
+  } else {
+    localStorage.setItem('token', '123')
+  }
+}
 </script>
 
 <template>
@@ -40,9 +47,10 @@ const handleLogin = () => {
         placeholder="Search a reference number to see shipment details"
         :prefix-icon="Search"
       />
-      <span class="font_family icon-icon_notice_b" style="font-size: 18px"></span>
-      <span class="font_family icon-icon_language_b" style="font-size: 16px"></span>
+      <!-- <span class="font_family icon-icon_notice_b" style="font-size: 18px"></span>
+      <span class="font_family icon-icon_language_b" style="font-size: 16px"></span> -->
 
+      <el-button @click="test">测试</el-button>
       <el-popover
         placement="bottom-end"
         :width="256"

+ 115 - 34
src/views/Layout/src/components/Menu/MenuView.vue

@@ -12,12 +12,12 @@ const menuList = [
     icon: 'icon_data_fill_b',
     path: '/dashboard'
   },
-  {
-    index: '2',
-    label: 'Quote',
-    icon: 'icon_quote__fill_b',
-    path: '/booking/detail'
-  },
+  // {
+  //   index: '2',
+  //   label: 'Quote',
+  //   icon: 'icon_quote__fill_b',
+  //   path: '/booking/detail'
+  // },
   {
     index: '3',
     label: 'Booking',
@@ -30,12 +30,12 @@ const menuList = [
     icon: 'icon_tracking__fill_b',
     path: '/tracking'
   },
-  {
-    index: '5',
-    label: 'Report',
-    icon: 'icon_report__fill_b',
-    path: '/tracking/detail'
-  },
+  // {
+  //   index: '5',
+  //   label: 'Report',
+  //   icon: 'icon_report__fill_b',
+  //   path: '/tracking/detail'
+  // },
   {
     index: '6',
     label: 'System Management',
@@ -43,21 +43,21 @@ const menuList = [
     path: '/test5',
     type: 'list',
     children: [
-      {
-        index: '5-1',
-        label: 'Account Management',
-        path: '/public-tracking'
-      },
-      {
-        index: '5-2',
-        label: 'Permission Management',
-        path: '/public-tracking/detail'
-      },
-      {
-        index: '5-3',
-        label: 'System Configuration',
-        path: '/login'
-      },
+      // {
+      //   index: '5-1',
+      //   label: 'Account Management',
+      //   path: '/public-tracking'
+      // },
+      // {
+      //   index: '5-2',
+      //   label: 'Permission Management',
+      //   path: '/public-tracking/detail'
+      // },
+      // {
+      //   index: '5-3',
+      //   label: 'System Configuration',
+      //   path: '/login'
+      // },
       {
         index: '5-4',
         label: 'Operation Log',
@@ -67,31 +67,107 @@ const menuList = [
   }
 ]
 
-const activeMenu = computed((): string => {
-  return (route.meta?.activeMenu as string) || route.path
+//监听窗口大小
+const handler = () => {
+  return (() => {
+    let screenWidth = document.body.clientWidth
+    let screenHeight = document.body.clientHeight
+    if (screenWidth < 1400) {
+      isCollapse.value = true
+    } else {
+      isCollapse.value = false
+    }
+  })()
+}
+const listeningWindow = () => {
+  window.addEventListener('resize', handler)
+}
+
+const unListeningWindow = () => {
+  window.removeEventListener('resize', handler)
+}
+
+watchEffect(() => {
+  listeningWindow()
 })
 
-const changeRouter = (path: string) => {
-  router.push({ path })
+onUnmounted(() => {
+  unListeningWindow()
+})
+
+const activeMenu = ref()
+activeMenu.value = (route.meta?.activeMenu as string) || route.path
+
+const getAllMenuPaths = (menuList: any) => {
+  let paths: any = []
+  menuList.forEach((item: any) => {
+    paths.push(item.path) // 添加主菜单路径
+    if (item.children && item.children.length > 0) {
+      // 递归添加子菜单路径
+      paths = paths.concat(getAllMenuPaths(item.children))
+    }
+  })
+  return paths
+}
+// 获取所有菜单项的路径(包括子菜单)
+const menuPaths = getAllMenuPaths(menuList)
+
+// 未登录白名单
+const whiteList = ['/login', '/public-tracking', '/public-tracking/detail']
+
+// 判断是否允许跳转
+const isAllowJump = (path: any) => {
+  // 判断是否登录
+  if (!whiteList.includes(path) && !localStorage.getItem('token')) {
+    ElMessage.warning('Please log in to use this feature.')
+    activeMenu.value = route.path // 保持选中状态不变
+    return false
+  }
+  return true
 }
+// 路由后置守卫
+router.afterEach(() => {
+  activeMenu.value = (route.meta?.activeMenu as string) || route.path
+})
+
+// 路由跳转函数
+const changeRouter = (path: any) => {
+  let toPath = path
+  if (path === '/tracking' && !localStorage.getItem('token')) {
+    toPath = '/public-tracking'
+  }
 
-const emit = defineEmits<{ collapse: [boolean] }>()
+  // 如果允许跳转,执行跳转
+  if (isAllowJump(toPath)) {
+    router.push(toPath)
+  } else {
+    // 如果不允许跳转,保持当前 activeMenu 不变
+    nextTick(() => {
+      activeMenu.value = (route.meta?.activeMenu as string) || route.path // 确保菜单栏选中状态为当前路径
+    })
+  }
+}
 
 const handleCollapseClick = () => {
   isCollapse.value = !isCollapse.value
-  // emit('collapse', isCollapse.value)
 }
+const menuRef = ref()
 </script>
 
 <template>
   <el-menu
+    ref="menuRef"
     class="layout-menu"
     @select="changeRouter"
     :default-active="activeMenu"
     :collapse="isCollapse"
   >
     <template v-for="item in menuList" :key="item.index">
-      <el-menu-item v-if="item.type !== 'list'" :index="item.path">
+      <el-menu-item
+        :class="{ 'clear-active-style': route.path === '/login' }"
+        v-if="item.type !== 'list'"
+        :index="item.path"
+      >
         <span class="font_family" :class="[`icon-${item.icon}`]"></span>
         <template #title>{{ item.label }}</template>
       </el-menu-item>
@@ -158,6 +234,11 @@ const handleCollapseClick = () => {
     padding-left: 8px;
   }
 }
+li.clear-active-style {
+  background-color: transparent !important;
+  color: var(--color-neutral-1) !important;
+  font-weight: normal !important;
+}
 
 :deep(.el-sub-menu__title) {
   height: 40px;

+ 79 - 0
src/views/Login/src/components/LoginCard.vue

@@ -0,0 +1,79 @@
+<script setup lang="ts"></script>
+
+<template>
+  <el-card class="login-card">
+    <div class="title">
+      <span class="welcome">Welcome to KLN Portal</span>
+      <span class="tips">Login to your account</span>
+    </div>
+    <div class="send-email-tips" :style="{ display: isEmailTips ? 'block' : 'none' }">
+      <span class="font_family icon-icon_confirm_b success-icon"></span>
+      New Password sent to registered email.
+      <span
+        @click="handleDeleteEmailTips"
+        class="font_family icon-icon_reject_b delete-icon"
+      ></span>
+    </div>
+    <div class="login-form">
+      <div class="label">
+        <span>User Name</span>
+      </div>
+      <el-input
+        ref="userNameRef"
+        :class="{ 'is-error': loginError.username }"
+        v-model="loginForm.username"
+        class="user-name"
+        placeholder="Please input user name"
+        @focus="handleDeleteEmailTips"
+        @change="isUserNameExit = true"
+      >
+        <template #prefix>
+          <span class="font_family icon-icon_username_b"></span>
+        </template>
+        <template #suffix>
+          <span v-if="isUserNameExit" class="font_family icon-icon_confirm_b confirm-icon"></span>
+        </template>
+      </el-input>
+      <div class="error" v-if="loginError.username">This account does not exist.</div>
+      <div class="label">
+        <span>Password</span>
+        <span class="forgot-password" @click="handleForgot">Forgot Password?</span>
+      </div>
+      <el-input
+        ref="passWordRef"
+        :class="{ 'is-error': loginError.password }"
+        v-model="loginForm.password"
+        type="password"
+        placeholder="Please input password"
+        show-password
+        @focus="handleDeleteEmailTips"
+        ><template #prefix>
+          <span class="font_family icon-icon_password_b"></span>
+        </template>
+      </el-input>
+      <div class="error" v-if="loginError.password">Incorrect password. Please try again.</div>
+      <el-input
+        ref="codeRef"
+        :class="{ 'is-error': loginError.code }"
+        class="verification-code"
+        v-model="loginForm.code"
+        placeholder="Verification Code"
+        @focus="handleDeleteEmailTips"
+      >
+        <template #append>
+          <img class="verification-code-img" src="./image/code.png" alt="" />
+        </template>
+      </el-input>
+      <div class="error" v-if="loginError.code">Incorrect verification code.</div>
+      <el-button @click="handleSubmit" class="el-button--dark login-btn">Login</el-button>
+    </div>
+    <template #footer>
+      <div class="license">
+        <span>© 2024 KTreker from <span class="company">Kerry Logistics</span></span>
+        <span>Version 0.67</span>
+      </div>
+    </template>
+  </el-card>
+</template>
+
+<style lang="scss" scoped></style>

+ 125 - 25
src/views/Login/src/loginView.vue

@@ -1,9 +1,11 @@
 <script setup lang="ts">
+import { useRouter } from 'vue-router'
 import ErrorTips from './components/ErrorTips.vue'
 
+const router = useRouter()
 const loginForm = ref({
-  username: '',
-  password: '',
+  username: 'ra.admin',
+  password: 'abc123456789',
   email: '',
   code: ''
 })
@@ -11,18 +13,105 @@ const loginForm = ref({
 const status = ref('login')
 watch(status, () => {
   loginForm.value = {
-    username: '',
-    password: '',
+    username: 'ra.admin',
+    password: 'abc123456789',
     email: '',
     code: ''
   }
+  loginError.value = {
+    username: false,
+    password: false,
+    email: false,
+    code: false
+  }
+  verificationCode.value = ''
+  getCode()
 })
 
-const loginError = ref({
+const loginError: any = ref({
   username: false,
   password: false,
+  email: false,
   code: false
 })
+const verificationCode = ref()
+// 获取验证码
+const getCode = () => {
+  $api.getVerifcationCode().then((res: any) => {
+    if (res.code === 200) {
+      verificationCode.value = `data:image/png;base64,${res.data.imagePngBase64}`
+    } else {
+      verificationCode.value = ''
+    }
+  })
+}
+getCode()
+
+// 验证当前用户是否存在
+const handleCheckUser = () => {
+  if (!loginForm.value.username) {
+    return
+  }
+  // 这里是验证用户是否存在的逻辑
+  $api.isUserNameExit({ uname: loginForm.value.username }).then((res: any) => {
+    if (res.code === 200) {
+      if (res.data.msg !== 'no_exist') {
+        isUserNameExit.value = true
+      } else {
+        loginError.value.username = true
+        isUserNameExit.value = false
+      }
+    } else {
+      isUserNameExit.value = false
+    }
+  })
+}
+
+// 点击登录按钮
+const handleLogin = () => {
+  // 这里是登录逻辑
+  $api
+    .login({
+      uname: loginForm.value.username,
+      psw: loginForm.value.password,
+      verifcation_code: loginForm.value.code
+    })
+    .then((res: any) => {
+      if (res.code === 200) {
+        const { data } = res
+        if (data.msg === 'today') {
+          ElMessageBox.alert('Your password will expire today, please reset', 'Prompt', {
+            confirmButtonText: 'OK',
+            type: 'warning',
+            confirmButtonClass: 'el-button--dark'
+          })
+        } else if (data.msg === 'last') {
+          ElMessageBox.alert(
+            `Your password will expire in${data.data}days, please reset`,
+            'Prompt',
+            {
+              confirmButtonText: 'OK',
+              type: 'warning',
+              confirmButtonClass: 'el-button--dark'
+            }
+          )
+        }
+        router.push('/')
+      } else if (res.code === 400) {
+        // 验证码错误
+        if (res.data.msg === 'password_error') {
+          loginError.value.password = true
+        } else if (res.data.msg === 'verifcation_error') {
+          loginError.value.code = true
+        } else if (res.data.msg === 'error_times') {
+          errorTipsRef.value.openDialog()
+        }
+      }
+    })
+    .finally(() => {
+      getCode()
+    })
+}
 
 const isUserNameExit = ref(false)
 
@@ -31,18 +120,26 @@ const handleForgot = () => {
   isUserNameExit.value = false
   handleDeleteEmailTips()
 }
-
-const handleSubmit = () => {
-  if (status.value === 'reset') {
-    isEmailTips.value = true
-    status.value = 'login'
-    return
-  }
+const handleSendPassword = () => {
+  // 这里是发送密码逻辑
+  $api
+    .forgotPassword({
+      login: loginForm.value.username,
+      email: loginForm.value.email
+    })
+    .then((res: any) => {
+      if (res.code === 200) {
+        isEmailTips.value = true
+      }
+    })
 }
 
 const isEmailTips = ref(false)
-const handleDeleteEmailTips = () => {
+const handleDeleteEmailTips = (type?: any) => {
   isEmailTips.value = false
+  if (type) {
+    loginError.value[type] = false
+  }
 }
 
 const errorTipsRef = ref()
@@ -73,8 +170,8 @@ const errorTipsRef = ref()
           v-model="loginForm.username"
           class="user-name"
           placeholder="Please input user name"
-          @focus="handleDeleteEmailTips"
-          @change="isUserNameExit = true"
+          @focus="handleDeleteEmailTips('username')"
+          @change="handleCheckUser"
         >
           <template #prefix>
             <span class="font_family icon-icon_username_b"></span>
@@ -95,7 +192,7 @@ const errorTipsRef = ref()
           type="password"
           placeholder="Please input password"
           show-password
-          @focus="handleDeleteEmailTips"
+          @focus="handleDeleteEmailTips('password')"
           ><template #prefix>
             <span class="font_family icon-icon_password_b"></span>
           </template>
@@ -107,14 +204,14 @@ const errorTipsRef = ref()
           class="verification-code"
           v-model="loginForm.code"
           placeholder="Verification Code"
-          @focus="handleDeleteEmailTips"
+          @focus="handleDeleteEmailTips('code')"
         >
           <template #append>
-            <img class="verification-code-img" src="./image/code.png" alt="" />
+            <img class="verification-code-img" :src="verificationCode" alt="" />
           </template>
         </el-input>
         <div class="error" v-if="loginError.code">Incorrect verification code.</div>
-        <el-button @click="handleSubmit" class="el-button--dark login-btn">Login</el-button>
+        <el-button @click="handleLogin" class="el-button--dark login-btn">Login</el-button>
       </div>
       <template #footer>
         <div class="license">
@@ -125,7 +222,7 @@ const errorTipsRef = ref()
     </el-card>
     <el-card class="login-card" v-else>
       <div class="title">
-        <span class="welcome"> Reset Password</span>
+        <span class="welcome">Password Retrieval</span>
         <span class="tips">We'll send you new password in email</span>
       </div>
       <div class="login-form">
@@ -138,7 +235,7 @@ const errorTipsRef = ref()
           v-model="loginForm.username"
           class="user-name"
           placeholder="Please input user name"
-          @focus="handleDeleteEmailTips"
+          @focus="handleDeleteEmailTips('username')"
         >
           <template #prefix>
             <span class="font_family icon-icon_username_b"></span>
@@ -155,10 +252,10 @@ const errorTipsRef = ref()
         </div>
         <el-input
           ref="passWordRef"
-          :class="{ 'is-error': loginError.password }"
+          :class="{ 'is-error': loginError.email }"
           v-model="loginForm.email"
           placeholder="Please input your email address"
-          show-password
+          @focus="handleDeleteEmailTips('email')"
           ><template #prefix>
             <span class="font_family icon-icon_email_b"></span>
           </template>
@@ -172,13 +269,16 @@ const errorTipsRef = ref()
           class="verification-code"
           v-model="loginForm.code"
           placeholder="Verification Code"
+          @focus="handleDeleteEmailTips('code')"
           ><template #append>
-            <img class="verification-code-img" src="./image/code.png" alt="" /> </template
+            <img class="verification-code-img" :src="verificationCode" alt="" /> </template
         ></el-input>
         <div class="error" v-if="loginError.code">
           This is the prompt information given by the verification
         </div>
-        <el-button @click="handleSubmit" class="el-button--dark login-btn">Send Password</el-button>
+        <el-button @click="handleSendPassword" class="el-button--dark login-btn"
+          >Send Password</el-button
+        >
         <div @click="status = 'login'" class="back-text">
           <span class="font_family icon-icon_back_b"></span>
           <span class="text"> Back to login</span>

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

@@ -1,5 +1,12 @@
 <script setup lang="ts">
+import { useRouter } from 'vue-router'
+const router = useRouter()
 const inputVModel = ref('')
+
+const handleSearchNo = () => {
+  router.push(`/public-tracking/detail?searchNo=${inputVModel.value}`)
+  console.log('search no')
+}
 </script>
 
 <template>
@@ -12,7 +19,7 @@ const inputVModel = ref('')
         placeholder="Search a reference number to see shipment details"
       >
         <template #append>
-          <span class="font_family icon-icon_search_b"></span>
+          <span @click="handleSearchNo" class="font_family icon-icon_search_b"></span>
         </template>
       </el-input>
       <div class="empty">

+ 0 - 13
src/views/Tracking/src/components/PublicTracking/src/components/PublicTrackingDetail.vue

@@ -1,14 +1,6 @@
 <script setup lang="ts">
 import BasicInformation from './BasicInformation.vue'
 import MilestonesTable from './MilestonesTable.vue'
-
-const test = (type: string) => {
-  ElMessage({
-    message: 'This is a message',
-    type: type,
-    duration: 0
-  })
-}
 </script>
 
 <template>
@@ -74,8 +66,6 @@ const test = (type: string) => {
         </template>
       </VBox>
     </div>
-    <el-button @click="test('error')">error</el-button>
-    <el-button @click="test('success')">success</el-button>
   </div>
 </template>
 
@@ -179,9 +169,6 @@ const test = (type: string) => {
       }
     }
   }
-  .transport-map {
-    margin: 20px 0;
-  }
 }
 
 .info-content {

+ 24 - 5
src/views/Tracking/src/components/TrackingDetail/src/TrackingDetail.vue

@@ -9,7 +9,9 @@ import TransportStep from './components/TransportStep.vue'
 import MilestonesTable from './components/MilestonesTable.vue'
 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'
 
 // 可拖拽模块的列表
 const boxList = ref([
@@ -79,7 +81,9 @@ const handleAMSISF = () => {
 }
 
 const allData = ref()
+const loading = ref(false)
 const getData = () => {
+  loading.value = true
   $api
     .getTrackingDetail({
       status: 'Confirmed',
@@ -87,12 +91,14 @@ const getData = () => {
       a: 'AjxXeBouEvrDQ6jYG3xp9V208RYJ6UrRpAH%2FRna8t%2BqjYnUcZnqOnvrE4Gg5'
     })
     .then((res: any) => {
-      console.log('res', res)
       if (res.code === 200) {
         // 获取数据
         allData.value = res.data
       }
     })
+    .finally(() => {
+      loading.value = false
+    })
 }
 getData()
 
@@ -102,10 +108,14 @@ const formatTime = (time: string) => {
 </script>
 
 <template>
-  <div class="tracking-detail">
+  <div class="tracking-detail" v-vloading="loading">
     <div class="header">
       <div class="detail-status">
-        <span class="font_family icon-icon_ocean_b" style="font-size: 64px"></span>
+        <span
+          class="font_family"
+          :class="[`icon-${transportationMode?.[allData?.transportInfo?.mode]}`]"
+          style="font-size: 64px"
+        ></span>
         <div class="no">Tracking No. {{ allData?.transportInfo?.['Tracking No.'] }}</div>
         <VTag large type="Confirmed">{{ allData?.transportInfo?.status }}</VTag>
         <div class="right-operation">
@@ -155,7 +165,8 @@ const formatTime = (time: string) => {
       </div>
     </div>
     <div class="transport-map">
-      <TransportStep :data="allData"></TransportStep>
+      <MapView></MapView>
+      <TransportStep class="transport-step" :data="allData"></TransportStep>
     </div>
     <div class="info-content">
       <VueDraggable
@@ -266,6 +277,7 @@ const formatTime = (time: string) => {
       display: flex;
       align-items: center;
       padding: 0 16px;
+      height: 64px;
       border-bottom: 1px solid var(--color-border);
 
       .right-operation {
@@ -356,7 +368,14 @@ const formatTime = (time: string) => {
     }
   }
   .transport-map {
-    margin: 20px 0;
+    position: relative;
+    margin-bottom: 8px;
+    .transport-step {
+      position: absolute;
+      top: 8px;
+      right: 16px;
+      z-index: 1000;
+    }
   }
 }
 

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

@@ -95,7 +95,7 @@ defineExpose({
 <template>
   <el-dialog v-model="dialogVisible" :width="800" title="Ref No." @closed="clearData">
     <div>
-      <el-button style="margin-bottom: 8px" type="text" @click="addNewReference">
+      <el-button style="margin-bottom: 8px" class="el-button-text" @click="addNewReference">
         <span class="font_family icon-icon_add_b"></span>
         Add Reference
       </el-button>

+ 0 - 2
src/views/Tracking/src/components/TrackingDetail/src/components/AttachmentView.vue

@@ -124,8 +124,6 @@ const handleDownload = (row: any) => {
   document.body.appendChild(link)
   link.click()
   document.body.removeChild(link)
-
-  console.log('Download', row.file)
 }
 const handleDelete = (row: any) => {
   console.log('Delete', row)

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

@@ -299,7 +299,7 @@ defineExpose({
           <!-- <el-button
             style="font-size: 12px"
             v-if="item.label === 'Ref No.'"
-            type="text"
+            class="el-button--text"
             @click="addReference"
           >
             <span class="font_family icon-icon_add_b"></span>

+ 0 - 13
src/views/Tracking/src/components/TrackingDetail/src/components/EmailDrawer.vue

@@ -313,16 +313,3 @@ defineExpose({
   }
 }
 </style>
-<style lang="scss">
-div.w-e-bar svg {
-  height: 16px;
-  width: 16px;
-}
-[data-menu-key='group-image'] {
-  & + .w-e-bar-item-menus-container {
-    .w-e-bar-item:first-child {
-      display: none;
-    }
-  }
-}
-</style>

+ 84 - 0
src/views/Tracking/src/components/TrackingDetail/src/components/MapView.vue

@@ -0,0 +1,84 @@
+<!-- src/components/MapView.vue -->
+<template>
+  <div id="map" style="width: 100%; height: 520px"></div>
+</template>
+<script setup lang="ts">
+import L from 'leaflet'
+import Location from '../images/location.png'
+
+const initMap = () => {
+  // 地图初始化
+  const map = L.map('map').setView([51.505, -0.09], 3)
+
+  // 添加 TileLayer
+  L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
+    attribution:
+      '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
+  }).addTo(map)
+
+  const popupOptions = {
+    closeButton: false, // 移除关闭按钮
+    autoClose: false, // 禁止点击其他地方自动关闭
+    closeOnClick: false // 禁止点击地图时自动关闭
+  }
+
+  const customIcon = L.icon({
+    iconUrl: Location,
+    iconSize: [20, 20], // 图标尺寸
+    iconAnchor: [10, 20], // 图标锚点
+    popupAnchor: [0, -8] // 弹出层位置
+  })
+
+  const marker = L.marker([51.5, -0.09], { icon: customIcon }).addTo(map)
+
+  const customPopupContent = `
+    <div class="popup-content">
+      <p class="label">origin</p>
+      <p>
+        <span class="font_family icon-icon_location_fill_b" style="color: #ED6D00"></span>
+        ShenZhen,SG
+      </p>
+    </div>
+  `
+
+  // 绑定弹出框并立即展示
+  marker.bindPopup(customPopupContent, popupOptions).openPopup()
+}
+
+onMounted(() => {
+  initMap()
+})
+</script>
+
+<style lang="scss">
+@import 'leaflet/dist/leaflet.css';
+.leaflet-popup-content-wrapper {
+  border-radius: 6px;
+}
+.leaflet-popup-content {
+  padding: 4px;
+  margin: 0;
+  .popup-content {
+    p {
+      margin: 0;
+      span {
+        margin-left: -2px;
+        font-size: 12px;
+        font-weight: 700;
+      }
+    }
+    .label {
+      margin-bottom: 4px;
+      font-size: 12px;
+      color: var(--color-neutral-2);
+    }
+  }
+}
+
+/* 自定义弹出窗口箭头 */
+.leaflet-popup-tip {
+  display: none;
+}
+.popup-content {
+}
+</style>

+ 9 - 5
src/views/Tracking/src/components/TrackingDetail/src/components/TransportStep.vue

@@ -29,12 +29,12 @@ const handleTabClick = (name: string) => {
       </div>
     </div>
     <div class="content">
-      <div v-if="activeName === 'shipmentStatus'">
+      <template v-if="activeName === 'shipmentStatus'">
         <ShipmentStatus :data="props.data?.simplexData" />
-      </div>
-      <div v-else-if="activeName === 'containerStatus'">
+      </template>
+      <template v-else-if="activeName === 'containerStatus'">
         <ContainerStatus :data="props.data?.containerStatusData" />
-      </div>
+      </template>
     </div>
   </div>
 </template>
@@ -42,6 +42,10 @@ const handleTabClick = (name: string) => {
 <style lang="scss" scoped>
 .transport-step {
   width: 400px;
+  height: 484px;
+  background-color: #fff;
+  border: 1px solid #eaebed;
+  border-radius: 3px;
   .header {
     display: flex;
     height: 48px;
@@ -62,7 +66,7 @@ const handleTabClick = (name: string) => {
     }
   }
   .content {
-    padding: 16px;
+    height: calc(100% - 48px);
   }
 }
 </style>

TEMPAT SAMPAH
src/views/Tracking/src/components/TrackingDetail/src/images/location.png


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

@@ -5,6 +5,7 @@ import { autoWidth } from '@/utils/table'
 import { useRowClickStyle } from '@/hooks/rowClickStyle'
 import dayjs from 'dayjs'
 import { useRouter } from 'vue-router'
+import { transportationMode } from '@/components/TransportationMode'
 
 const router = useRouter()
 const props = defineProps({
@@ -250,7 +251,7 @@ const handleCustomizeColumns = () => {
       model_name: 'Ocean_Search'
     }
   }
-  CustomizeColumnsRef.value.openDialog(params, -194)
+  CustomizeColumnsRef.value.openDialog(params, -153)
 }
 
 // 定制表格
@@ -265,7 +266,7 @@ const customizeColumns = async () => {
 const handleCellDblclick = ({ row }: any) => {
   router.push({
     path: '/tracking/detail',
-    query: { id: row.h_bol }
+    query: { a: row.__serial_no, _schemas: row.__schemas }
   })
 }
 // 点击link字段是时
@@ -273,12 +274,12 @@ const handleLinkClick = (row: any, column: any) => {
   if (column.field === 'booking_no') {
     router.push({
       path: '/booking/detail',
-      query: { id: row.booking_no }
+      query: { a: row.__serial_no, _schemas: row.__schemas }
     })
   } else if (column.field === 'h_bol') {
     router.push({
       path: '/tracking/detail',
-      query: { id: row.h_bol }
+      query: { a: row.__serial_no, _schemas: row.__schemas }
     })
   }
 }
@@ -343,9 +344,13 @@ defineExpose({
         </el-button>
       </template>
       <!-- Transportation Mode字段的插槽 -->
-      <template #mode>
+      <template #mode="{ row, column }">
         <div>
-          <span class="font_family icon-icon_ocean_b" style="font-size: 24px"></span>
+          <span
+            class="font_family"
+            :class="[`icon-${transportationMode?.[row[column.field]]}`]"
+            style="font-size: 24px"
+          ></span>
         </div>
       </template>
       <!-- Status字段的插槽 -->