AmandaG пре 1 година
родитељ
комит
27d93e6bbd
56 измењених фајлова са 2605 додато и 565 уклоњено
  1. 1 0
      .env
  2. 1 0
      .env.test
  3. 1 1
      .prettierrc.json
  4. 6 3
      package.json
  5. 2 2
      src/api/module/booking.ts
  6. 21 2
      src/api/module/common.ts
  7. 60 1
      src/api/module/login.ts
  8. 61 1
      src/api/module/tracking.ts
  9. 1 1
      src/components/ScoringGrade/src/ScoringGrade.vue
  10. 6 0
      src/components/VBreadcrumd/src/VBreadcrumd.vue
  11. 8 4
      src/components/VEmpty/src/VEmpty.vue
  12. 25 4
      src/router/index.ts
  13. 35 0
      src/stores/modules/headerSearch.ts
  14. 29 0
      src/stores/modules/user.ts
  15. 44 0
      src/styles/elementui.scss
  16. 3 0
      src/utils/axios.ts
  17. 1 1
      src/utils/table.ts
  18. 38 9
      src/views/Booking/src/components/BookingDetail/src/BookingDetail.vue
  19. 9 7
      src/views/Booking/src/components/BookingDetail/src/components/BasicInformation.vue
  20. 4 2
      src/views/Booking/src/components/BookingDetail/src/components/ContainersView.vue
  21. 10 8
      src/views/Booking/src/components/BookingTable/src/BookingTable.vue
  22. 0 11
      src/views/Booking/src/components/BookingTable/src/components/DownloadDialog.vue
  23. 4 4
      src/views/Dashboard/src/components/ScoringSystem.vue
  24. 103 22
      src/views/Layout/src/components/Header/HeaderView.vue
  25. 179 6
      src/views/Layout/src/components/Header/components/ChangePasswordDialog.vue
  26. 12 1
      src/views/Layout/src/components/Header/components/LogoutDialog.vue
  27. 6 8
      src/views/Layout/src/components/Menu/MenuView.vue
  28. 421 0
      src/views/Login/src/components/ChangePasswordCard.vue
  29. 0 79
      src/views/Login/src/components/LoginCard.vue
  30. BIN
      src/views/Login/src/image/tips.png
  31. 45 21
      src/views/Login/src/loginView.vue
  32. 1 1
      src/views/OperationLog/src/components/BookingTable/src/BookingTable.vue
  33. 0 3
      src/views/Tracking/src/TrackingView.vue
  34. 69 8
      src/views/Tracking/src/components/PublicTracking/src/PublicTrackingSearch.vue
  35. 69 15
      src/views/Tracking/src/components/PublicTracking/src/components/BasicInformation.vue
  36. 52 36
      src/views/Tracking/src/components/PublicTracking/src/components/MilestonesTable.vue
  37. 83 11
      src/views/Tracking/src/components/PublicTracking/src/components/PublicTrackingDetail.vue
  38. 74 11
      src/views/Tracking/src/components/TrackingDetail/src/TrackingDetail.vue
  39. 75 79
      src/views/Tracking/src/components/TrackingDetail/src/components/AMS&ISF.vue
  40. 4 20
      src/views/Tracking/src/components/TrackingDetail/src/components/AttachmentView.vue
  41. 8 6
      src/views/Tracking/src/components/TrackingDetail/src/components/BasicInformation.vue
  42. 4 2
      src/views/Tracking/src/components/TrackingDetail/src/components/ContainersView.vue
  43. 187 30
      src/views/Tracking/src/components/TrackingDetail/src/components/MapView.vue
  44. 6 10
      src/views/Tracking/src/components/TrackingDetail/src/components/MilestonesTable.vue
  45. 164 89
      src/views/Tracking/src/components/TrackingDetail/src/components/RoutesView.vue
  46. 15 5
      src/views/Tracking/src/components/TrackingDetail/src/components/UploadFilesDialog.vue
  47. 0 0
      src/views/Tracking/src/components/TrackingDetail/src/images/destinationIcon.png
  48. BIN
      src/views/Tracking/src/components/TrackingDetail/src/images/originIcon.png
  49. BIN
      src/views/Tracking/src/components/TrackingDetail/src/images/transferIcon.png
  50. 74 20
      src/views/Tracking/src/components/TrackingTable/src/TrackingTable.vue
  51. 0 11
      src/views/Tracking/src/components/TrackingTable/src/components/DownloadDialog.vue
  52. 565 0
      src/views/Tracking/src/components/TrackingTable/src/components/VGMView.vue
  53. 3 1
      tsconfig.app.json
  54. 9 3
      tsconfig.json
  55. 6 5
      tsconfig.node.json
  56. 1 1
      vite.config.ts

+ 1 - 0
.env

@@ -0,0 +1 @@
+VITE_API_HOST = 'http://localhost/api'

+ 1 - 0
.env.test

@@ -0,0 +1 @@
+VITE_API_HOST = 'http://192.168.0.161'

+ 1 - 1
.prettierrc.json

@@ -5,4 +5,4 @@
   "singleQuote": true,
   "printWidth": 100,
   "trailingComma": "none"
-}
+}

+ 6 - 3
package.json

@@ -5,9 +5,11 @@
   "type": "module",
   "scripts": {
     "dev": "vite",
+    "dev:test": "vite --host --mode test",
     "build": "run-p type-check \"build-only {@}\" --",
     "preview": "vite preview",
     "build-only": "vite build",
+    "build:test": "vite build --mode test",
     "type-check": "vue-tsc --build --force",
     "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
     "format": "prettier --write src/"
@@ -28,6 +30,7 @@
     "mitt": "^3.0.1",
     "moment": "^2.30.1",
     "pinia": "^2.2.2",
+    "sass-loader": "^16.0.2",
     "vue": "^3.4.29",
     "vue-draggable-plus": "^0.5.3",
     "vue-router": "^4.3.3",
@@ -43,7 +46,7 @@
     "@types/leaflet": "^1.9.12",
     "@types/lodash": "^4.17.7",
     "@types/node": "^20.14.5",
-    "@vitejs/plugin-vue": "^5.0.5",
+    "@vitejs/plugin-vue": "^5.1.4",
     "@vue/eslint-config-prettier": "^9.0.0",
     "@vue/eslint-config-typescript": "^13.0.0",
     "@vue/tsconfig": "^0.5.1",
@@ -54,10 +57,10 @@
     "postcss": "^8.4.41",
     "postcss-loader": "^8.1.1",
     "prettier": "^3.2.5",
-    "sass": "^1.77.8",
+    "sass": "^1.79.4",
     "typescript": "~5.4.0",
     "unplugin-auto-import": "^0.18.2",
-    "unplugin-icons": "^0.19.2",
+    "unplugin-icons": "^0.19.3",
     "unplugin-vue-components": "^0.27.4",
     "vite": "^5.3.1",
     "vue-tsc": "^2.0.21"

+ 2 - 2
src/api/module/booking.ts

@@ -1,6 +1,7 @@
 import HttpAxios from '@/utils/axios'
 
-const baseUrl = 'http://localhost/api/Customer_Service_Online/main_new_version.php'
+const base = import.meta.env.VITE_API_HOST
+const baseUrl = `${base}/Customer_Service_Online/main_new_version.php`
 
 /**
  * Booking首页表格列数据
@@ -41,7 +42,6 @@ export const getBookingDetail = (params: any, config: any) => {
     {
       action: 'ocean_booking',
       operate: 'detail',
-      _schemas: 'public',
       ...params
     },
     config

+ 21 - 2
src/api/module/common.ts

@@ -1,6 +1,7 @@
 import HttpAxios from '@/utils/axios'
 
-const baseUrl = 'http://localhost/api/Customer_Service_Online/main_new_version.php'
+const base = import.meta.env.VITE_API_HOST
+const baseUrl = `${base}/Customer_Service_Online/main_new_version.php`
 
 /**
  * 获取表格定制列功能数据
@@ -24,6 +25,21 @@ export const saveTableSettingColumns = (params: any, config: any) => {
     config
   )
 }
+
+/**
+ * 修改密码
+ */
+export const changePwdByLogin = (params: any, config: any) => {
+  return HttpAxios.post(
+    `${baseUrl}`,
+    {
+      action: 'password',
+      ...params
+    },
+    config
+  )
+}
+
 /**
  * 获取日志列
  */
@@ -31,7 +47,7 @@ export const getOperationTableColumns = (params: any, config: any) => {
   return HttpAxios.get(
     `${baseUrl}`,
     {
-      action: 'opreation_log',
+      action: 'opreation_log'
     },
     config
   )
@@ -172,6 +188,7 @@ export const SaveLayout = (params: any, config: any) => {
     config
   )
 }
+<<<<<<< HEAD
 /**
  * ETD to ETA(DAYS)点击跳转
  */
@@ -292,3 +309,5 @@ export const scoringgrade = (params: any, config: any) => {
     config
   )
 }
+=======
+>>>>>>> dev

+ 60 - 1
src/api/module/login.ts

@@ -1,6 +1,7 @@
 import HttpAxios from '@/utils/axios'
 
-const baseUrl = 'http://localhost/api/Customer_Service_Online/login.php'
+const base = import.meta.env.VITE_API_HOST
+const baseUrl = `${base}/Customer_Service_Online/login.php`
 
 /**
  * 获取验证码
@@ -47,6 +48,19 @@ export const login = (params: any, config: any) => {
   )
 }
 
+// 退出登录
+export const logout = (params: any, config: any) => {
+  return HttpAxios.post(
+    `${baseUrl}`,
+    {
+      action: 'login',
+      operate: 'logout',
+      ...params
+    },
+    config
+  )
+}
+
 /**
  * 忘记密码
  */
@@ -61,3 +75,48 @@ export const forgotPassword = (params: any, config: any) => {
     config
   )
 }
+
+/**
+ * 更新密码
+ */
+export const changePassword = (params: any, config: any) => {
+  return HttpAxios.post(
+    `${baseUrl}`,
+    {
+      action: 'login',
+      operate: 'update_pwd_expires',
+      ...params
+    },
+    config
+  )
+}
+
+/**
+ * 获取public tracking detail详情数据
+ */
+export const getPublicTrackingDetail = (params: any, config: any) => {
+  return HttpAxios.post(
+    `${baseUrl}`,
+    {
+      action: 'login',
+      operate: 'tracking_checked',
+      ...params
+    },
+    config
+  )
+}
+
+/**
+ * reset password
+ */
+export const resetPwd = (params: any, config: any) => {
+  return HttpAxios.post(
+    `${baseUrl}`,
+    {
+      action: 'login',
+      operate: 'update_pwd_expires',
+      ...params
+    },
+    config
+  )
+}

+ 61 - 1
src/api/module/tracking.ts

@@ -1,6 +1,7 @@
 import HttpAxios from '@/utils/axios'
 
-const baseUrl = 'http://localhost/api/Customer_Service_Online/main_new_version.php'
+const base = import.meta.env.VITE_API_HOST
+const baseUrl = `${base}/Customer_Service_Online/main_new_version.php`
 
 /**
  * Tracking首页表格列数据
@@ -47,3 +48,62 @@ export const getTrackingDetail = (params: any, config: any) => {
     config
   )
 }
+
+/**
+ * 获取Tracking详情页中地图数据
+ */
+export const getTrackingDetailMapData = (params: any, config: any) => {
+  return HttpAxios.get(
+    `${baseUrl}`,
+    {
+      action: 'main_map_new',
+      ...params
+    },
+    config
+  )
+}
+
+/**
+ * 获取Tracking详情页中AMS/ISF表格数据
+ */
+export const getTrackingAmsIsf = (params: any, config: any) => {
+  return HttpAxios.get(
+    `${baseUrl}`,
+    {
+      action: 'ocean_order',
+      operate: 'ams_isf_log',
+      ...params
+    },
+    config
+  )
+}
+
+/**
+ * 获取add vgm页面数据
+ */
+export const getVGMData = (params: any, config: any) => {
+  return HttpAxios.get(
+    `${baseUrl}`,
+    {
+      action: 'ocean_order',
+      operate: 'ocean_vgm',
+      ...params
+    },
+    config
+  )
+}
+
+/**
+ * 保存 vgm页面数据
+ */
+export const saveVGMData = (params: any, config: any) => {
+  return HttpAxios.post(
+    `${baseUrl}`,
+    {
+      action: 'ocean_order',
+      operate: 'save_ocean_vgm',
+      ...params
+    },
+    config
+  )
+}

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

@@ -384,4 +384,4 @@ const submitDetails = (val: any) => {
 .result {
   height: 578px;
 }
-</style>
+</style>

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

@@ -17,6 +17,12 @@ const path = computed(() => {
       // 获取上一段字符串
       return path.substring(lastSlashIndex + 1, detailIndex)
     }
+  } else if (path.includes('/add-vgm')) {
+    const detailIndex = path.indexOf('/add-vgm')
+    const lastSlashIndex = path.lastIndexOf('/', detailIndex - 1)
+    if (lastSlashIndex !== -1) {
+      return path.substring(lastSlashIndex + 1, detailIndex)
+    }
   }
   // 如果没有找到或者不符合条件,则返回null
   return null

+ 8 - 4
src/components/VEmpty/src/VEmpty.vue

@@ -5,11 +5,15 @@
     <div class="empty-img">
       <img src="./images/default_image.png" alt="" />
     </div>
-    <p class="title">Whoops, No matches</p>
-    <p class="light">We didn't find any search results,</p>
-    <p class="light">please try to adjust your search keywords.</p>
+    <p class="title">
+      <slot name="title">No Results Found</slot>
+    </p>
+    <slot name="result">
+      <p class="light">We didn't find any search results,</p>
+      <p class="light">please try to adjust your search keywords.</p>
+    </slot>
     <div class="suggestion">
-      <slot></slot>
+      <slot name="suggestion"></slot>
     </div>
   </div>
 </template>

+ 25 - 4
src/router/index.ts

@@ -1,12 +1,13 @@
 import { createRouter, createWebHistory } from 'vue-router'
+import { useUserStore } from '@/stores/modules/user'
 
 const router = createRouter({
-  history: createWebHistory(import.meta.env.BASE_URL),
+  history: createWebHistory('/k_new_online/'),
   routes: [
     {
       path: '/',
       name: 'Home',
-      redirect: '/booking',
+      redirect: '/dashboard',
       component: () => import('../views/Layout'),
       children: [
         {
@@ -40,6 +41,15 @@ const router = createRouter({
             activeMenu: '/tracking'
           }
         },
+        {
+          path: '/tracking/add-vgm',
+          name: 'Add VGM',
+          component: () =>
+            import('../views/Tracking/src/components/TrackingTable/src/components/VGMView.vue'),
+          meta: {
+            activeMenu: '/tracking'
+          }
+        },
         {
           path: '/public-tracking',
           name: 'Public Tracking',
@@ -67,6 +77,11 @@ const router = createRouter({
             activeMenu: '/tracking'
           }
         },
+        {
+          path: '/reset-password',
+          name: 'Reset Password',
+          component: () => import('../views/Login/src/components/ChangePasswordCard.vue')
+        },
         {
           path: '/Operationlog',
           name: 'Operationlog',
@@ -80,9 +95,11 @@ const router = createRouter({
 // * 路由拦截 beforeEach
 router.beforeEach(async (to, from, next) => {
   // 未登录白名单
-  const whiteList = ['/login', '/public-tracking', '/public-tracking/detail']
+  const whiteList = ['/login', '/public-tracking', '/public-tracking/detail', '/reset-password']
   // 判断是否登录
-  if (!whiteList.includes(to.path) && !localStorage.getItem('token')) {
+  if (!whiteList.includes(to.path) && !localStorage.getItem('username')) {
+    const userStore = useUserStore()
+    userStore.clearUsername()
     if (whiteList.includes(from.path)) {
       ElMessage.warning('Please log in to use this feature.')
       next(false)
@@ -91,6 +108,10 @@ router.beforeEach(async (to, from, next) => {
       next('/public-tracking')
     }
   }
+  if (to.path === '/Login') {
+    const userStore = useUserStore()
+    userStore.clearUsername()
+  }
   next()
 })
 

+ 35 - 0
src/stores/modules/headerSearch.ts

@@ -0,0 +1,35 @@
+import { defineStore } from 'pinia'
+
+interface HeaderSearch {
+  searchValue: string
+  searchResult?: string
+  isChangeByLogin: boolean
+}
+export const useHeaderSearch = defineStore('headerSearch', {
+  state: (): HeaderSearch => ({
+    searchValue: JSON.parse(localStorage.getItem('searchData'))?.searchValue || '',
+    searchResult: JSON.parse(localStorage.getItem('searchData'))?.searchResult || '',
+    isChangeByLogin: Boolean(localStorage.getItem('isChangeByLogin')) || false
+  }),
+  getters: {},
+  actions: {
+    setSearchData(searchData: any) {
+      localStorage.setItem('searchData', JSON.stringify(searchData))
+      this.searchValue = searchData.searchValue
+      this.searchResult = searchData.searchResult
+    },
+    setChangeByLogin(isChangeByLogin: boolean) {
+      localStorage.setItem('isChangeByLogin', JSON.stringify(isChangeByLogin))
+      this.isChangeByLogin = isChangeByLogin
+    },
+    clearSearchData() {
+      localStorage.removeItem('searchData')
+      this.searchValue = ''
+      this.searchResult = ''
+    },
+    clearChangeByLogin() {
+      localStorage.removeItem('isChangeByLogin')
+      this.isChangeByLogin = false
+    }
+  }
+})

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

@@ -0,0 +1,29 @@
+import { defineStore } from 'pinia'
+
+interface UserState {
+  username: string
+}
+export const useUserStore = defineStore('user', {
+  state: (): UserState => ({
+    username: localStorage.getItem('username') || ''
+  }),
+  getters: {},
+  actions: {
+    setUsername(username: any) {
+      localStorage.setItem('username', username)
+      this.username = username
+    },
+    logout() {
+      $api.logout().then((res: any) => {
+        if (res.code === 200) {
+          localStorage.removeItem('username')
+          this.username = ''
+        }
+      })
+    },
+    clearUsername() {
+      localStorage.removeItem('username')
+      this.username = ''
+    }
+  }
+})

+ 44 - 0
src/styles/elementui.scss

@@ -442,6 +442,50 @@ div .el-badge {
   margin: 8px 0 0 8px;
 }
 
+.el-date-table td.current:not(.disabled) span.el-date-table-cell__text {
+  background-color: var(--color-theme);
+}
+.el-month-table td.current:not(.disabled) span.el-date-table-cell__text {
+  background-color: var(--color-theme);
+}
+.el-month-table td span.el-date-table-cell__text:hover {
+  color: var(--color-theme);
+}
+.el-year-table td.current:not(.disabled) span.el-date-table-cell__text {
+  background-color: var(--color-theme);
+}
+.el-year-table td.today span.el-date-table-cell__text {
+  color: var(--color-theme);
+}
+.el-year-table td span.el-date-table-cell__text:hover {
+  color: var(--color-theme);
+}
+div.el-date-picker__header {
+  padding-top: 4px;
+}
+span.el-date-picker__header-label {
+  display: inline-block;
+  margin-top: 11px;
+}
+div.el-picker-panel__content {
+  margin-top: 6px;
+}
+button.el-picker-panel__icon-btn:hover {
+  color: var(--color-theme);
+}
+span.el-date-picker__header-label:hover {
+  color: var(--color-theme);
+}
+button.el-time-panel__btn.confirm {
+  color: var(--color-theme);
+}
+.el-picker-panel__footer {
+  .el-button.is-plain {
+    --el-button-hover-text-color: var(--color-theme);
+    --el-button-hover-border-color: var(--color-theme);
+  }
+}
+
 div .el-table--striped .el-table__body tr.el-table__row--striped td.el-table__cell {
   background-color: var(--color-table-header-bg);
 }

+ 3 - 0
src/utils/axios.ts

@@ -1,6 +1,7 @@
 import axios, { type AxiosInstance, type AxiosRequestConfig, type AxiosResponse } from 'axios'
 import router from '@/router'
 import { ElMessage, ElMessageBox } from 'element-plus'
+import { useUserStore } from '@/stores/modules/user'
 // import {
 //   showFullScreenLoading,
 //   tryHideFullScreenLoading
@@ -55,6 +56,8 @@ class HttpAxios {
   _responseInterceptors = (response: AxiosResponse) => {
     if (response.status === 200) {
       if (response.data.code === 401 || response.data.code === 403) {
+        const userStore = useUserStore()
+        userStore.clearUsername()
         router.push('/login')
         ElMessage.warning('Please log in to use this feature.')
       } else if (response.data.code !== 200 && response.data.code !== 400) {

+ 1 - 1
src/utils/table.ts

@@ -17,7 +17,7 @@ export const autoWidth = (tableData: VxeGridProps, grid: VxeGridInstance) => {
       let width = 0
       const field = column.field
       // 判断表头的宽度
-      if (column.title.length < 10) {
+      if (column.title.length < 12) {
         width = column.title.length * 11 + 40
       } else if (column.title.length < 20) {
         width = column.title.length * 8 + 40

+ 38 - 9
src/views/Booking/src/components/BookingDetail/src/BookingDetail.vue

@@ -6,6 +6,9 @@ import ContainersView from './components/ContainersView.vue'
 import EmailView from './components/EmailView.vue'
 import { cloneDeep } from 'lodash'
 import { transportationMode } from '@/components/TransportationMode'
+import { useRoute } from 'vue-router'
+
+const route = useRoute()
 
 // 可拖拽模块的列表
 const boxList = ref([
@@ -60,7 +63,6 @@ const handleDraggable = (type: string, id: number) => {
       break
   }
 }
-
 const allData = ref()
 const loading = ref(false)
 const getData = () => {
@@ -68,8 +70,8 @@ const getData = () => {
   $api
     .getBookingDetail({
       status: 'Confirmed',
-      // a: 'UjwCcE8rEPXDQ6jYG3xp9V208RYJ6UrRpAH%2FRna8t%2BqjYnUcZnqOnvrE4Gg',
-      a: 'AjxXeBouEvrDQ6jYG3xp9V208RYJ6UrRpAH%2FRna8t%2BqjYnUcZnqOnvrE4Gg5'
+      a: route.query.a,
+      _schemas: route.query._schemas
     })
     .then((res: any) => {
       if (res.code === 200) {
@@ -102,19 +104,30 @@ const formatTime = (time: string) => {
       </div>
       <div class="detail-info">
         <div class="item transport-way">
-          <div class="place">
+          <div class="origin">
             <div class="title">Origin</div>
             <div class="content">
-              <span>{{ allData?.transportInfo?.origin }}</span>
+              <!-- <span>{{ allData?.transportInfo?.origin }}</span> -->
+              <el-tooltip placement="top">
+                <template #content>{{ allData?.transportInfo?.origin }}</template>
+                <span class="info single-line-ellipsis">{{ allData?.transportInfo?.origin }}</span>
+              </el-tooltip>
               <div class="line_container">
                 <hr color="#000000" />
                 <div class="right-icon"></div>
               </div>
             </div>
           </div>
-          <div class="place">
+          <div class="destination">
             <div class="title">Destination</div>
-            <div class="content">{{ allData?.transportInfo?.destination }}</div>
+            <div class="content">
+              <el-tooltip placement="top">
+                <template #content>{{ allData?.transportInfo?.destination }}</template>
+                <span class="info single-line-ellipsis">{{
+                  allData?.transportInfo?.destination
+                }}</span>
+              </el-tooltip>
+            </div>
           </div>
         </div>
         <div class="item">
@@ -254,8 +267,15 @@ const formatTime = (time: string) => {
         flex-direction: row;
         justify-content: flex-start;
         gap: 16px;
-        .place {
-          flex: 1;
+        max-width: 500px;
+        .origin {
+          width: 60%;
+        }
+        .destination {
+          width: 40%;
+          .info {
+            width: calc(100% - 16px);
+          }
         }
         .title {
           margin-top: 11px;
@@ -271,6 +291,7 @@ const formatTime = (time: string) => {
           .line_container {
             flex: 1;
             position: relative;
+            min-width: 26px;
             margin-left: 16px;
           }
           .line_container hr {
@@ -315,6 +336,7 @@ const formatTime = (time: string) => {
   justify-content: flex-start;
   align-items: flex-start;
   gap: 8px;
+  min-height: 38px;
   .title {
     font-size: 12px;
     color: var(--color-neutral-2);
@@ -326,4 +348,11 @@ const formatTime = (time: string) => {
     color: var(--color-neutral-1);
   }
 }
+
+.single-line-ellipsis {
+  display: inline-block; /* 或者根据需要使用 inline-block */
+  white-space: nowrap; /* 不换行 */
+  overflow: hidden; /* 隐藏超出部分 */
+  text-overflow: ellipsis; /* 超出部分显示省略号 */
+}
 </style>

+ 9 - 7
src/views/Booking/src/components/BookingDetail/src/components/BasicInformation.vue

@@ -263,18 +263,20 @@ watch(
 // 跳转到shipment页面
 const handLink = (id: string) => {
   router.push({
-    path: '/tracking',
+    path: '/tracking/detail',
     query: { a: props?.data?.__serial_no, _schemas: props?.data?._schemas }
   })
 }
 
 const handleCopy = (data: any) => {
-  const sanitizedData = data.map((item: string) =>
-    item
-      .replace(/[\r\n]+/g, ' ')
-      .replace(/\s+/g, ' ')
-      .trim()
-  ) // 用空格替换换行符
+  const sanitizedData = data.map((item: string) => {
+    return item
+      ? item
+          .replace(/[\r\n]+/g, ' ')
+          .replace(/\s+/g, ' ')
+          .trim()
+      : ''
+  }) // 用空格替换换行符
   const copyText = sanitizedData.join('\n') // 拼接为单行文本
   if (XEClipboard.copy(copyText)) {
     ElMessage.success('Copy success')

+ 4 - 2
src/views/Booking/src/components/BookingDetail/src/components/ContainersView.vue

@@ -40,12 +40,14 @@ const handleColumns = (columns: any) => {
         sortBy: ({ row, column }: any) => {
           return dayjs(row[column.field]).unix()
         },
-        formatter: ({ cellValue }: any) => dayjs(cellValue).format('MMM-YYYY-DD ') || '--'
+        formatter: ({ cellValue }: any) =>
+          cellValue ? dayjs(cellValue).format('MMM-YYYY-DD ') : '--'
       }
     } else if (item.formatter === 'dateTime') {
       curColumn = {
         ...curColumn,
-        formatter: ({ cellValue }: any) => dayjs(cellValue).format('MMM-YYYY-DD HH:mm:ss') || '--'
+        formatter: ({ cellValue }: any) =>
+          cellValue ? dayjs(cellValue).format('MMM-YYYY-DD HH:mm:ss') : '--'
       }
     }
     return curColumn

+ 10 - 8
src/views/Booking/src/components/BookingTable/src/BookingTable.vue

@@ -52,12 +52,14 @@ const handleColumns = (columns: any, status?: string) => {
         sortBy: ({ row, column }: any) => {
           return dayjs(row[column.field]).unix()
         },
-        formatter: ({ cellValue }: any) => dayjs(cellValue).format('MMM-YYYY-DD ') || '--'
+        formatter: ({ cellValue }: any) =>
+          cellValue ? dayjs(cellValue).format('MMM-YYYY-DD ') : '--'
       }
     } else if (item.formatter === 'dateTime') {
       curColumn = {
         ...curColumn,
-        formatter: ({ cellValue }: any) => dayjs(cellValue).format('MMM-YYYY-DD HH:mm:ss') || '--'
+        formatter: ({ cellValue }: any) =>
+          cellValue ? dayjs(cellValue).format('MMM-YYYY-DD HH:mm:ss') : '--'
       }
     }
     return curColumn
@@ -307,20 +309,20 @@ const customizeColumns = async () => {
 const handleCellDblclick = ({ row }: any) => {
   router.push({
     path: '/booking/detail',
-    query: { a: row.__serial_no, _schemas: row.__schemas }
+    query: { a: row.__serial_no, _schemas: row._schemas }
   })
 }
 // 点击link字段是时
 const handleLinkClick = (row: any, column: any) => {
-  if (column.field === 'booking_no') {
+  if (column.title === 'Booking No.') {
     router.push({
       path: '/booking/detail',
-      query: { a: row.__serial_no, _schemas: row.__schemas }
+      query: { a: row.__serial_no, _schemas: row._schemas }
     })
-  } else if (column.field === 'h_bol') {
+  } else if (column.title === 'HBL No.') {
     router.push({
       path: '/tracking/detail',
-      query: { a: row.__serial_no, _schemas: row.__schemas }
+      query: { a: row.__serial_no, _schemas: row._schemas }
     })
   }
 }
@@ -368,7 +370,7 @@ defineExpose({
       <!-- 空数据时的插槽 -->
       <template #empty>
         <VEmpty>
-          <template #default>
+          <template #suggestion>
             <p style="color: var(--color-neutral-3)">
               We support the following references number to find booking:
             </p>

+ 0 - 11
src/views/Booking/src/components/BookingTable/src/components/DownloadDialog.vue

@@ -140,17 +140,6 @@ defineExpose({
       font-size: 12px;
     }
 
-    .el-icon {
-      margin-left: 4px;
-      color: var(--color-theme);
-      transform: rotate(0deg);
-      transition: all 0.3s;
-
-      &.is-rotate {
-        transform: rotate(180deg);
-      }
-    }
-
     .select-columns {
       padding: 8px;
       margin-top: 8px;

+ 4 - 4
src/views/Dashboard/src/components/ScoringSystem.vue

@@ -23,9 +23,9 @@ const avater = ref<AvaterItem[]>([])
 const inputdetails = ref('')
 avater.value = [
   {
-    src: '/src/styles/images/score_angry.png',
-    src1: '/src/styles/images/score_angry.png',
-    itemsrc: '/src/styles/images/angry_2.png',
+    src: '/k_new_online/src/styles/images/score_angry.png',
+    src1: '/k_new_online/src/styles/images/score_angry.png',
+    itemsrc: '/k_new_online/src/styles/images/angry_2.png',
     itemtext: 'We are so sorry for the inconveniences. We value your experience immensely. ',
     expression: 'angry',
     proposal: 'Could you please tell us which aspects of the system you are dissatisfied with?',
@@ -618,4 +618,4 @@ const submitDetails = (val: any) => {
 .el-radio {
   margin-right: 0;
 }
-</style>
+</style>

+ 103 - 22
src/views/Layout/src/components/Header/HeaderView.vue

@@ -4,9 +4,71 @@ import DownloadKLNPortal from './components/DownloadKLNPortal.vue'
 import ChangePasswordDialog from './components/ChangePasswordDialog.vue'
 import LogoutDialog from './components/LogoutDialog.vue'
 import { useRouter } from 'vue-router'
+import { useUserStore } from '@/stores/modules/user'
+import { useHeaderSearch } from '@/stores/modules/headerSearch'
 
+const userStore = useUserStore()
 const router = useRouter()
-const input1 = ref('')
+const headerSearch = useHeaderSearch()
+
+const searchValue = ref('A1703530062')
+const handleSearch = () => {
+  // 先判断是否登录
+  // 未登录
+  if (!localStorage.getItem('username')) {
+    $api.getPublicTrackingDetail({ reference_number: searchValue.value }).then((res) => {
+      if (res.code === 200) {
+        const { data } = res
+        if (data.msg === 'No matches') {
+          headerSearch.setSearchData({
+            searchValue: searchValue.value,
+            searchResult: 'error'
+          })
+          router.push({
+            name: 'Public Tracking'
+          })
+        } else if (data.msg === 'Multiple results') {
+          headerSearch.setSearchData({
+            searchValue: searchValue.value,
+            searchResult: 'multiple'
+          })
+          router.push({
+            name: 'Public Tracking'
+          })
+        } else {
+          sessionStorage.setItem('publicTrackingData', JSON.stringify(data.data))
+          router.push(`/public-tracking/detail?searchNo=${searchValue.value}`)
+        }
+      }
+    })
+  } else {
+    // 已登录
+    $api
+      .getTrackingTableData({
+        _textSearch: searchValue.value
+      })
+      .then((res) => {
+        if (res.code === 200) {
+          const { searchData } = res.data
+          if (searchData.length === 1) {
+            router.push({
+              name: 'Tracking Detail',
+              query: {
+                a: res.data.searchData[0].__serial_no,
+                _schemas: res.data.searchData[0].__schemas
+              }
+            })
+          } else if (searchData.length !== 1) {
+            headerSearch.setChangeByLogin(true)
+            localStorage.setItem('TrackingData', JSON.stringify(res.data))
+            router.push({
+              name: 'Tracking'
+            })
+          }
+        }
+      })
+  }
+}
 
 const downloadKLNPortalRef = ref()
 const handleDownload = () => {
@@ -27,14 +89,6 @@ const handleLogout = () => {
 const handleLogin = () => {
   router.push('/login')
 }
-
-const test = () => {
-  if (localStorage.getItem('token')) {
-    localStorage.removeItem('token')
-  } else {
-    localStorage.setItem('token', '123')
-  }
-}
 </script>
 
 <template>
@@ -42,15 +96,15 @@ const test = () => {
     <VBreadcrumd></VBreadcrumd>
     <div class="right-info">
       <el-input
-        v-model="input1"
+        v-model="searchValue"
         size="large"
+        @keyup.enter="handleSearch"
         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> -->
 
-      <el-button @click="test">测试</el-button>
       <el-popover
         placement="bottom-end"
         :width="256"
@@ -59,11 +113,8 @@ const test = () => {
         content="this is content, this is content, this is content"
       >
         <div class="title">
-          <el-avatar
-            :size="48"
-            src="https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png"
-          />
-          <span class="name">John Doe</span>
+          <div class="avatar">{{ userStore.username?.slice(0, 1) }}</div>
+          <span class="name">{{ userStore.username }}</span>
         </div>
         <div class="options">
           <div class="item" @click="handleChangePassword">
@@ -76,17 +127,24 @@ const test = () => {
           </div>
         </div>
         <template #reference>
-          <el-avatar
-            :size="24"
-            src="https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png"
-          />
+          <div class="avatar" v-if="userStore.username">{{ userStore.username?.slice(0, 1) }}</div>
         </template>
       </el-popover>
-      <el-button class="el-button--main" style="padding: 8px 10px" plain @click="handleDownload">
+      <el-button
+        v-if="!userStore.username"
+        class="el-button--main"
+        style="padding: 8px 10px"
+        plain
+        @click="handleDownload"
+      >
         <span class="font_family icon-icon_download_b" style="margin-right: 4px"></span>
         Download KLN Portal
       </el-button>
-      <el-button class="el-button--main" style="margin-left: -10px" @click="handleLogin"
+      <el-button
+        v-if="!userStore.username"
+        class="el-button--main"
+        style="margin-left: -10px"
+        @click="handleLogin"
         >Log in</el-button
       >
     </div>
@@ -97,6 +155,18 @@ const test = () => {
 </template>
 
 <style lang="scss" scoped>
+.avatar {
+  width: 24px;
+  height: 24px;
+  border-radius: 50%;
+  text-align: center;
+  line-height: 24px;
+  color: #fff;
+  font-size: 16px;
+  font-weight: 700;
+  background-color: var(--color-theme);
+  cursor: pointer;
+}
 .layout-toolbar {
   display: flex;
   justify-content: space-between;
@@ -140,6 +210,17 @@ div.el-popover.el-popper.user-config-popover {
     gap: 8px;
     height: 70px;
     border-bottom: 1px solid #eeeeed;
+    .avatar {
+      width: 48px;
+      height: 48px;
+      border-radius: 50%;
+      text-align: center;
+      line-height: 48px;
+      color: #fff;
+      font-size: 32px;
+      font-weight: 700;
+      background-color: var(--color-theme);
+    }
   }
   .options {
     .item {

+ 179 - 6
src/views/Layout/src/components/Header/components/ChangePasswordDialog.vue

@@ -5,18 +5,105 @@ const openDialog = () => {
   dialogVisible.value = true
 }
 
-const data = ref({
+const pwdData = ref({
   oldPassword: '',
   newPassword: '',
   confirmPassword: ''
 })
+const loginError: any = ref({
+  oldPassword: false,
+  newPassword: false,
+  confirmPassword: false
+})
+const newPwdErrTips = ref('')
+
+const handleUpdate = () => {
+  if (pwdData.value.newPassword !== pwdData.value.confirmPassword) {
+    loginError.value.confirmPassword = true
+    return
+  }
+  if (pwdData.value.newPassword.length < 12 || pwdData.value.newPassword.length > 20) {
+    loginError.value.newPassword = true
+    newPwdErrTips.value = 'Password length between 12 - 20'
+    return
+  }
+  if (!/[A-Z]/.test(pwdData.value.newPassword)) {
+    loginError.value.newPassword = true
+    newPwdErrTips.value = 'Password must contain uppercase letters'
+    return
+  }
+  if (!/[a-z]/.test(pwdData.value.newPassword)) {
+    loginError.value.newPassword = true
+    newPwdErrTips.value = 'Password must contain lowercase letters'
+    return
+  }
+  if (!/[0-9]/.test(pwdData.value.newPassword)) {
+    loginError.value.newPassword = true
+    newPwdErrTips.value = 'Password must contain numbers'
+    return
+  }
+  $api
+    .changePwdByLogin({
+      opsw: pwdData.value.oldPassword,
+      npsw: pwdData.value.newPassword
+    })
+    .then((res) => {
+      if (res.code === 200) {
+        ElMessage.success('Password updated successfully')
+        dialogVisible.value = false
+      } else if (res.code === 400) {
+        if (res.msg === 'Old password is incorrect') {
+          loginError.value.oldPassword = true
+        } else {
+          loginError.value.newPassword = true
+          newPwdErrTips.value = res.msg
+        }
+      }
+    })
+}
+
+const hiddenError = (key: string) => {
+  loginError.value[key] = false
+}
+const confirmPwd = () => {
+  if (pwdData.value.confirmPassword !== pwdData.value.newPassword) {
+    loginError.value.confirmPassword = true
+  } else {
+    loginError.value.confirmPassword = false
+  }
+}
+
+const hasUppercase = ref(false)
+const hasLowercase = ref(false)
+const hasNumber = ref(false)
+const isValidLength = ref(false)
+const checkPassword = () => {
+  const pwd = pwdData.value.newPassword
+
+  // 检测是否包含大写字母
+  hasUppercase.value = /[A-Z]/.test(pwd)
+
+  // 检测是否包含小写字母
+  hasLowercase.value = /[a-z]/.test(pwd)
+
+  // 检测是否包含数字
+  hasNumber.value = /[0-9]/.test(pwd)
+
+  // 检测长度是否符合要求
+  isValidLength.value = pwd.length >= 12 && pwd.length <= 20
+}
 
 const clearData = () => {
-  data.value = {
+  pwdData.value = {
     oldPassword: '',
     newPassword: '',
     confirmPassword: ''
   }
+  loginError.value = {
+    oldPassword: false,
+    newPassword: false,
+    confirmPassword: false
+  }
 }
 defineExpose({
   openDialog
@@ -35,37 +122,82 @@ defineExpose({
     <div>
       <div class="form-item">
         <span class="label">Old Password</span>
-        <el-input v-model="data.oldPassword" placeholder="Please input Old Password">
+        <el-input
+          v-model="pwdData.oldPassword"
+          type="password"
+          show-password
+          :class="{ 'is-error': loginError.oldPassword }"
+          placeholder="Please input Old Password"
+          @focus="hiddenError('oldPassword')"
+        >
           <template #prefix>
             <span class="font_family icon-icon_password_b"></span>
           </template>
         </el-input>
+        <div class="error" v-if="loginError.oldPassword">Incorrect password. Please try again.</div>
       </div>
       <div class="form-item">
         <span class="label">New Password</span>
         <el-input
-          v-model="data.newPassword"
+          v-model="pwdData.newPassword"
+          type="password"
+          show-password
+          :class="{ 'is-error': loginError.newPassword }"
           placeholder="New password must contain both letter and numeral"
+          @focus="hiddenError('newPassword')"
+          @input="checkPassword"
         >
           <template #prefix>
             <span class="font_family icon-icon_password_b"></span>
           </template>
         </el-input>
+        <div class="error" v-if="loginError.newPassword">{{ newPwdErrTips }}</div>
+        <div class="limit-tips">
+          <div class="tip-item">
+            <span class="font_family icon-icon_confirm_b" :class="{ active: hasUppercase }"></span>
+            <span>Password must contain uppercase letters</span>
+          </div>
+          <div class="tip-item">
+            <span class="font_family icon-icon_confirm_b" :class="{ active: hasLowercase }"></span>
+            <span>Password must contain lowercase letters</span>
+          </div>
+          <div class="tip-item">
+            <span class="font_family icon-icon_confirm_b" :class="{ active: hasNumber }"></span>
+            <span>Password must contain numbers</span>
+          </div>
+          <div class="tip-item">
+            <span class="font_family icon-icon_confirm_b" :class="{ active: isValidLength }"></span>
+            <span>Password length between12 - 20 </span>
+          </div>
+        </div>
       </div>
       <div class="form-item">
         <span class="label">Confirm Password</span>
-        <el-input v-model="data.confirmPassword" placeholder="Please Confirm Password">
+        <el-input
+          v-model="pwdData.confirmPassword"
+          type="password"
+          show-password
+          :class="{ 'is-error': loginError.confirmPassword }"
+          placeholder="Please Confirm Password"
+          @focus="hiddenError('confirmPassword')"
+          @blur="confirmPwd"
+        >
           <template #prefix>
             <span class="font_family icon-icon_password_b"></span>
           </template>
         </el-input>
+        <div class="error" v-if="loginError.confirmPassword">
+          The password does not match. Please try again.
+        </div>
       </div>
     </div>
     <template #footer>
       <el-button class="el-button--default" style="height: 40px" @click="dialogVisible = false"
         >Cancel</el-button
       >
-      <el-button class="el-button--dark" style="height: 40px">Update</el-button>
+      <el-button @click="handleUpdate" class="el-button--dark" style="height: 40px"
+        >Update</el-button
+      >
     </template>
   </el-dialog>
 </template>
@@ -80,6 +212,47 @@ defineExpose({
     .label {
       font-size: 12px;
     }
+    .error {
+      font-size: 12px;
+      color: var(--color-danger);
+      line-height: 14px;
+    }
+    .limit-tips {
+      margin-top: 10px;
+      .tip-item {
+        display: flex;
+        align-items: center;
+        margin-bottom: 8px;
+        font-size: 12px;
+        color: var(--color-neutral-2);
+        .font_family {
+          display: flex;
+          justify-content: center;
+          align-items: center;
+          height: 12px;
+          width: 12px;
+          padding-top: 1px;
+          background-color: #b5b9bf;
+          font-size: 10px;
+          color: white;
+          border-radius: 50%;
+          &.active {
+            background-color: #00a870;
+          }
+        }
+        span {
+          margin-left: 4px;
+        }
+      }
+    }
+    .el-input {
+      height: 40px;
+      &.is-error {
+        .el-input__wrapper {
+          box-shadow: 0 0 0 1px var(--color-danger) inset;
+        }
+      }
+    }
   }
   .el-button {
     padding: 8px 24px;

+ 12 - 1
src/views/Layout/src/components/Header/components/LogoutDialog.vue

@@ -1,9 +1,20 @@
 <script setup lang="ts">
+import { useRouter } from 'vue-router'
+import { useUserStore } from '@/stores/modules/user'
+
+const router = useRouter()
 const dialogVisible = ref(false)
 
 const openDialog = () => {
   dialogVisible.value = true
 }
+
+const userStore = useUserStore()
+const handleLogout = () => {
+  dialogVisible.value = false
+  userStore.logout()
+  router.push('/login')
+}
 defineExpose({
   openDialog
 })
@@ -17,7 +28,7 @@ defineExpose({
     </div>
     <template #footer>
       <el-button class="el-button--default" @click="dialogVisible = false">Cancel</el-button>
-      <el-button class="el-button--dark">Logout</el-button>
+      <el-button class="el-button--dark" @click="handleLogout">Logout</el-button>
     </template>
   </el-dialog>
 </template>

+ 6 - 8
src/views/Layout/src/components/Menu/MenuView.vue

@@ -40,7 +40,6 @@ const menuList = [
     index: '6',
     label: 'System Management',
     icon: 'icon_system__management_fill_b',
-    path: '/test5',
     type: 'list',
     children: [
       // {
@@ -71,7 +70,6 @@ const menuList = [
 const handler = () => {
   return (() => {
     let screenWidth = document.body.clientWidth
-    let screenHeight = document.body.clientHeight
     if (screenWidth < 1400) {
       isCollapse.value = true
     } else {
@@ -109,16 +107,14 @@ const getAllMenuPaths = (menuList: any) => {
   })
   return paths
 }
-// 获取所有菜单项的路径(包括子菜单)
-const menuPaths = getAllMenuPaths(menuList)
 
 // 未登录白名单
-const whiteList = ['/login', '/public-tracking', '/public-tracking/detail']
+const whiteList = ['/login', '/public-tracking', '/public-tracking/detail', '/reset-password']
 
 // 判断是否允许跳转
 const isAllowJump = (path: any) => {
   // 判断是否登录
-  if (!whiteList.includes(path) && !localStorage.getItem('token')) {
+  if (!whiteList.includes(path) && !localStorage.getItem('username')) {
     ElMessage.warning('Please log in to use this feature.')
     activeMenu.value = route.path // 保持选中状态不变
     return false
@@ -133,7 +129,7 @@ router.afterEach(() => {
 // 路由跳转函数
 const changeRouter = (path: any) => {
   let toPath = path
-  if (path === '/tracking' && !localStorage.getItem('token')) {
+  if (path === '/tracking' && !localStorage.getItem('username')) {
     toPath = '/public-tracking'
   }
 
@@ -164,7 +160,9 @@ const menuRef = ref()
   >
     <template v-for="item in menuList" :key="item.index">
       <el-menu-item
-        :class="{ 'clear-active-style': route.path === '/login' }"
+        :class="{
+          'clear-active-style': route.path === '/login' || route.path === '/reset-password'
+        }"
         v-if="item.type !== 'list'"
         :index="item.path"
       >

+ 421 - 0
src/views/Login/src/components/ChangePasswordCard.vue

@@ -0,0 +1,421 @@
+<script setup lang="ts">
+import { useRouter } from 'vue-router'
+
+const router = useRouter()
+const loginForm = ref({
+  username: 'ra.admin',
+  oldPassword: '',
+  newPassword: '',
+  confirmPassword: ''
+})
+
+const newPwdErrTips = ref('')
+const loginError: any = ref({
+  oldPassword: false,
+  newPassword: false,
+  confirmPassword: false
+})
+
+const handleChangePwd = () => {
+  if (loginForm.value.newPassword !== loginForm.value.confirmPassword) {
+    loginError.value.confirmPassword = true
+    return
+  }
+  if (loginForm.value.newPassword.length < 12 || loginForm.value.newPassword.length > 20) {
+    loginError.value.newPassword = true
+    newPwdErrTips.value = 'Password length between 12 - 20'
+    return
+  }
+  if (!/[A-Z]/.test(loginForm.value.newPassword)) {
+    loginError.value.newPassword = true
+    newPwdErrTips.value = 'Password must contain uppercase letters'
+    return
+  }
+  if (!/[a-z]/.test(loginForm.value.newPassword)) {
+    loginError.value.newPassword = true
+    newPwdErrTips.value = 'Password must contain lowercase letters'
+    return
+  }
+  if (!/[0-9]/.test(loginForm.value.newPassword)) {
+    loginError.value.newPassword = true
+    newPwdErrTips.value = 'Password must contain numbers'
+    return
+  }
+  $api
+    .resetPwd({
+      uname: loginForm.value.username,
+      old_password: loginForm.value.oldPassword,
+      password: loginForm.value.newPassword
+    })
+    .then((res: any) => {
+      if (res.code === 200) {
+        router.push({
+          name: 'Login',
+          query: {
+            status: 'login'
+          }
+        })
+      } else if (res.code === 400) {
+        if (res.msg === 'Old password is incorrect') {
+          loginError.value.oldPassword = true
+        } else {
+          loginError.value.newPassword = true
+          newPwdErrTips.value = res.msg
+        }
+      }
+    })
+}
+
+const hiddenError = (key: string) => {
+  loginError.value[key] = false
+}
+const confirmPwd = () => {
+  if (loginForm.value.confirmPassword !== loginForm.value.newPassword) {
+    loginError.value.confirmPassword = true
+  } else {
+    loginError.value.confirmPassword = false
+  }
+}
+
+const isUserNameExit = ref(false)
+
+const handleForgot = () => {
+  router.push({
+    name: 'Login',
+    query: {
+      status: 'reset'
+    }
+  })
+}
+const handleBackLogin = () => {
+  router.push({
+    name: 'Login',
+    query: {
+      status: 'login'
+    }
+  })
+}
+
+const hasUppercase = ref(false)
+const hasLowercase = ref(false)
+const hasNumber = ref(false)
+const isValidLength = ref(false)
+const checkPassword = () => {
+  const pwd = loginForm.value.newPassword
+
+  // 检测是否包含大写字母
+  hasUppercase.value = /[A-Z]/.test(pwd)
+
+  // 检测是否包含小写字母
+  hasLowercase.value = /[a-z]/.test(pwd)
+
+  // 检测是否包含数字
+  hasNumber.value = /[0-9]/.test(pwd)
+
+  // 检测长度是否符合要求
+  isValidLength.value = pwd.length >= 12 && pwd.length <= 20
+}
+</script>
+
+<template>
+  <div class="login">
+    <el-card class="login-card">
+      <div class="title">
+        <span class="welcome">Change Password</span>
+        <span class="tips">Password expired, please change your password.</span>
+      </div>
+
+      <div class="login-form">
+        <div class="label">
+          <span>User Name</span>
+        </div>
+        <el-input :disabled="true" ref="userNameRef" v-model="loginForm.username" class="user-name">
+          <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="label">
+          <span>Old Password</span>
+          <span class="forgot-password" @click="handleForgot">Forgot Password?</span>
+        </div>
+        <el-input
+          ref="passWordRef"
+          :class="{ 'is-error': loginError.oldPassword }"
+          v-model="loginForm.oldPassword"
+          type="password"
+          placeholder="Please input password"
+          show-password
+          @focus="hiddenError('oldPassword')"
+        >
+          <template #prefix>
+            <span class="font_family icon-icon_password_b"></span>
+          </template>
+        </el-input>
+        <div class="error" v-if="loginError.oldPassword">Incorrect password. Please try again.</div>
+        <div class="label">
+          <span>New Password</span>
+        </div>
+        <el-input
+          ref="passWordRef"
+          :class="{ 'is-error': loginError.newPassword }"
+          v-model="loginForm.newPassword"
+          type="password"
+          placeholder="Please input password"
+          show-password
+          @focus="hiddenError('newPassword')"
+          @input="checkPassword"
+        >
+          <template #prefix>
+            <span class="font_family icon-icon_password_b"></span>
+          </template>
+        </el-input>
+        <div class="error" v-if="loginError.newPassword">{{ newPwdErrTips }}</div>
+        <div class="limit-tips">
+          <div class="tip-item">
+            <span class="font_family icon-icon_confirm_b" :class="{ active: hasUppercase }"></span>
+            <span>Password must contain uppercase letters</span>
+          </div>
+          <div class="tip-item">
+            <span class="font_family icon-icon_confirm_b" :class="{ active: hasLowercase }"></span>
+            <span>Password must contain lowercase letters</span>
+          </div>
+          <div class="tip-item">
+            <span class="font_family icon-icon_confirm_b" :class="{ active: hasNumber }"></span>
+            <span>Password must contain numbers</span>
+          </div>
+          <div class="tip-item">
+            <span class="font_family icon-icon_confirm_b" :class="{ active: isValidLength }"></span>
+            <span>Password length between12 - 20 </span>
+          </div>
+        </div>
+        <div class="label">
+          <span>Confirm New Password</span>
+        </div>
+
+        <el-input
+          ref="passWordRef"
+          :class="{ 'is-error': loginError.confirmPassword }"
+          v-model="loginForm.confirmPassword"
+          type="password"
+          placeholder="Please input password"
+          show-password
+          @focus="hiddenError('confirmPassword')"
+          @blur="confirmPwd"
+        >
+          <template #prefix>
+            <span class="font_family icon-icon_password_b"></span>
+          </template>
+        </el-input>
+        <div class="error" v-if="loginError.confirmPassword">
+          The password does not match. Please try again.
+        </div>
+        <el-button @click="handleChangePwd" class="el-button--dark login-btn"
+          >Change Password</el-button
+        >
+        <div @click="handleBackLogin" class="back-text">
+          <span class="font_family icon-icon_back_b"></span>
+          <span class="text"> Back to login</span>
+        </div>
+      </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>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.login {
+  position: relative;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  height: 100%;
+  width: 100%;
+  background: url(../image/bg-image.png) no-repeat center center;
+  background-size: cover;
+}
+
+.login-card {
+  width: 400px;
+
+  .title {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    margin-bottom: 24px;
+
+    .welcome {
+      margin-bottom: 16px;
+      font-size: 24px;
+      font-weight: 700;
+    }
+  }
+
+  :deep(.el-card__body) {
+    padding: 40px;
+    padding-bottom: 16px;
+  }
+
+  .login-btn {
+    width: 100%;
+    height: 40px;
+    margin-top: 16px;
+  }
+}
+
+.login-form {
+  display: flex;
+  flex-direction: column;
+  align-items: flex-start;
+
+  .el-input {
+    height: 40px;
+    .confirm-icon {
+      display: inline-flex;
+      align-items: center;
+      justify-content: center;
+      height: 16px;
+      width: 16px;
+      margin-right: 4px;
+      font-size: 14px;
+      color: #fff;
+      background-color: var(--color-success);
+      border-radius: 50%;
+    }
+
+    &.is-error {
+      :deep(.el-input__wrapper) {
+        box-shadow: 0 0 0 1px var(--color-danger) inset;
+      }
+    }
+
+    :deep(.el-input__prefix) {
+      margin: 0 4px;
+      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);
+        font-weight: 700;
+      }
+    }
+  }
+  .limit-tips {
+    margin-top: 10px;
+    .tip-item {
+      display: flex;
+      align-items: center;
+      margin-bottom: 8px;
+      font-size: 12px;
+      color: var(--color-neutral-2);
+      &:nth-last-child(1) {
+        margin-bottom: 0;
+      }
+      .font_family {
+        display: flex;
+        justify-content: center;
+        align-items: center;
+        height: 12px;
+        width: 12px;
+        padding-top: 1px;
+        background-color: #b5b9bf;
+        font-size: 10px;
+        color: white;
+        border-radius: 50%;
+        &.active {
+          background-color: #00a870;
+        }
+      }
+      span {
+        margin-left: 4px;
+      }
+    }
+  }
+  .verification-code {
+    margin-top: 16px;
+    .verification-code-img {
+      width: 130px;
+      height: 38px;
+      object-fit: cover;
+    }
+    :deep(.el-input-group__append) {
+      padding: 0;
+      padding-right: 1px;
+    }
+  }
+  .el-input.user-name {
+    :deep(.el-input__wrapper) {
+      padding-right: 6px;
+    }
+  }
+
+  .label {
+    display: flex;
+    justify-content: space-between;
+    width: 100%;
+    margin-top: 16px;
+    font-size: 12px;
+    line-height: 18px;
+
+    span {
+      color: var(--color-neutral-2);
+    }
+
+    .forgot-password {
+      color: var(--color-theme);
+      cursor: pointer;
+    }
+  }
+
+  .error {
+    font-size: 12px;
+    color: var(--color-danger);
+    line-height: 14px;
+  }
+
+  .back-text {
+    width: 100%;
+    height: 20px;
+    margin-top: 24px;
+    margin-bottom: 8px;
+    text-align: center;
+    cursor: pointer;
+
+    span {
+      color: var(--color-theme);
+    }
+
+    .text {
+      display: inline-block;
+      transform: translateY(-2px);
+      font-size: 12px;
+    }
+  }
+}
+
+.license {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  font-size: 12px;
+
+  .company {
+    color: var(--color-theme);
+  }
+
+  span {
+    color: var(--color-neutral-2);
+  }
+}
+</style>

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

@@ -1,79 +0,0 @@
-<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>

BIN
src/views/Login/src/image/tips.png


+ 45 - 21
src/views/Login/src/loginView.vue

@@ -1,8 +1,11 @@
 <script setup lang="ts">
-import { useRouter } from 'vue-router'
+import { useRouter, useRoute } from 'vue-router'
 import ErrorTips from './components/ErrorTips.vue'
+import { useUserStore } from '@/stores/modules/user'
+import ScoringSystem from '@/views/Dashboard/src/components/ScoringSystem.vue'
 
 const router = useRouter()
+const route = useRoute()
 const loginForm = ref({
   username: 'ra.admin',
   password: 'abc123456789',
@@ -10,7 +13,7 @@ const loginForm = ref({
   code: ''
 })
 
-const status = ref('login')
+const status = ref(route.query.status || 'login')
 watch(status, () => {
   loginForm.value = {
     username: 'ra.admin',
@@ -35,15 +38,22 @@ const loginError: any = ref({
   code: false
 })
 const verificationCode = ref()
+const loading = ref(false)
 // 获取验证码
 const getCode = () => {
-  $api.getVerifcationCode().then((res: any) => {
-    if (res.code === 200) {
-      verificationCode.value = `data:image/png;base64,${res.data.imagePngBase64}`
-    } else {
-      verificationCode.value = ''
-    }
-  })
+  loading.value = true
+  $api
+    .getVerifcationCode()
+    .then((res: any) => {
+      if (res.code === 200) {
+        verificationCode.value = `data:image/png;base64,${res.data.imagePngBase64}`
+      } else {
+        verificationCode.value = ''
+      }
+    })
+    .finally(() => {
+      loading.value = false
+    })
 }
 getCode()
 
@@ -67,6 +77,8 @@ const handleCheckUser = () => {
   })
 }
 
+const userStore = useUserStore()
+
 // 点击登录按钮
 const handleLogin = () => {
   // 这里是登录逻辑
@@ -96,6 +108,7 @@ const handleLogin = () => {
             }
           )
         }
+        userStore.setUsername(res.data.uname || '')
         router.push('/')
       } else if (res.code === 400) {
         // 验证码错误
@@ -147,6 +160,7 @@ const errorTipsRef = ref()
 
 <template>
   <div class="login">
+    <ScoringSystem class="scoring-system"></ScoringSystem>
     <el-card class="login-card" v-if="status === 'login'">
       <div class="title">
         <span class="welcome">Welcome to KLN Portal</span>
@@ -171,7 +185,7 @@ const errorTipsRef = ref()
           class="user-name"
           placeholder="Please input user name"
           @focus="handleDeleteEmailTips('username')"
-          @change="handleCheckUser"
+          @blur="handleCheckUser"
         >
           <template #prefix>
             <span class="font_family icon-icon_username_b"></span>
@@ -207,7 +221,12 @@ const errorTipsRef = ref()
           @focus="handleDeleteEmailTips('code')"
         >
           <template #append>
-            <img class="verification-code-img" :src="verificationCode" alt="" />
+            <img
+              v-vloading="loading"
+              class="verification-code-img"
+              :src="verificationCode"
+              alt=""
+            />
           </template>
         </el-input>
         <div class="error" v-if="loginError.code">Incorrect verification code.</div>
@@ -220,7 +239,7 @@ const errorTipsRef = ref()
         </div>
       </template>
     </el-card>
-    <el-card class="login-card" v-else>
+    <el-card class="login-card" v-else-if="status === 'reset'">
       <div class="title">
         <span class="welcome">Password Retrieval</span>
         <span class="tips">We'll send you new password in email</span>
@@ -236,6 +255,7 @@ const errorTipsRef = ref()
           class="user-name"
           placeholder="Please input user name"
           @focus="handleDeleteEmailTips('username')"
+          @blur="handleCheckUser"
         >
           <template #prefix>
             <span class="font_family icon-icon_username_b"></span>
@@ -244,9 +264,7 @@ const errorTipsRef = ref()
             <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 is the prompt information given by the verification
-        </div>
+        <div class="error" v-if="loginError.username">This account does not exist</div>
         <div class="label">
           <span>Email Address</span>
         </div>
@@ -260,9 +278,7 @@ const errorTipsRef = ref()
             <span class="font_family icon-icon_email_b"></span>
           </template>
         </el-input>
-        <div class="error" v-if="loginError.password">
-          This is the prompt information given by the verification
-        </div>
+        <div class="error" v-if="loginError.email">Incorrect email. Please try again.</div>
         <el-input
           ref="codeRef"
           :class="{ 'is-error': loginError.code }"
@@ -273,9 +289,7 @@ const errorTipsRef = ref()
           ><template #append>
             <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>
+        <div class="error" v-if="loginError.code">Incorrect verification code.</div>
         <el-button @click="handleSendPassword" class="el-button--dark login-btn"
           >Send Password</el-button
         >
@@ -291,18 +305,27 @@ const errorTipsRef = ref()
         </div>
       </template>
     </el-card>
+
     <ErrorTips ref="errorTipsRef" @forget-password="status = 'reset'"></ErrorTips>
   </div>
 </template>
 
 <style lang="scss" scoped>
 .login {
+  position: relative;
   display: flex;
   justify-content: center;
   align-items: center;
   height: 100%;
+  width: 100%;
   background: url(../src/image/bg-image.png) no-repeat center center;
   background-size: cover;
+  .scoring-system {
+    position: absolute;
+    top: 0;
+    width: 100%;
+    background: linear-gradient(251deg, #fff4eb 22.66%, #f0f3ff 44.57%, #e0f7f9 70.46%);
+  }
 }
 
 .login-card {
@@ -397,6 +420,7 @@ const errorTipsRef = ref()
   .verification-code {
     margin-top: 16px;
     .verification-code-img {
+      display: block;
       width: 130px;
       height: 38px;
       object-fit: cover;

+ 1 - 1
src/views/OperationLog/src/components/BookingTable/src/BookingTable.vue

@@ -257,7 +257,7 @@ defineExpose({
       @checkbox-all="handleCheckAllChange"
     >
       <!-- 空数据时的插槽 -->
-      <template #empty>
+      <template #suggestion>
         <VEmpty>
           <template #default>
             <p style="color: var(--color-neutral-3)">

+ 0 - 3
src/views/Tracking/src/TrackingView.vue

@@ -12,7 +12,6 @@ import { useRouter } from 'vue-router'
 const router = useRouter()
 
 const filterRef: Ref<HTMLElement | null> = ref(null)
-// const containerHeight: any = ref(0)
 
 const containerHeight = useCalculatingHeight(document.documentElement, 246, [filterRef])
 
@@ -323,8 +322,6 @@ const SearchInput = () => {
     :tagsData="tagsData"
     ref="TrackingTable_ref"
   ></TrackingTable>
-  <!-- <ShipmentStatus></ShipmentStatus> -->
-  <!-- <ContainerStatus></ContainerStatus> -->
 </template>
 
 <style lang="scss" scoped>

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

@@ -1,16 +1,60 @@
 <script setup lang="ts">
-import { useRouter } from 'vue-router'
+import { useRouter, useRoute } from 'vue-router'
+import { useHeaderSearch } from '@/stores/modules/headerSearch'
+
 const router = useRouter()
-const inputVModel = ref('')
+const route = useRoute()
+
+const headerSearch = useHeaderSearch()
+const searchResult = ref('')
+
+const inputVModel = ref('A170353006211')
+// 从 store 中获取数据并绑定到输入框
+const headerSearchdData = computed(() => headerSearch.searchValue)
+
+// 监听 sharedData 的变化并更新 inputValue
+headerSearchdData.value && (inputVModel.value = headerSearchdData.value)
+searchResult.value = headerSearch.searchResult
+
+// 当 sharedData 发生变化时,更新 inputValue
+watch(
+  () => headerSearchdData.value,
+  (newData) => {
+    // if (newData) {
+    inputVModel.value = headerSearchdData.value
+    searchResult.value = headerSearch.searchResult
+    // headerSearch.clearSearchData()
+    // }
+  }
+)
 
+const loading = ref(false)
 const handleSearchNo = () => {
-  router.push(`/public-tracking/detail?searchNo=${inputVModel.value}`)
-  console.log('search no')
+  loading.value = true
+  $api
+    .getPublicTrackingDetail({ reference_number: inputVModel.value })
+    .then((res) => {
+      if (res.code === 200) {
+        const { data } = res
+        if (data.msg === 'No matches') {
+          searchResult.value = 'error'
+        } else if (data.msg === 'Multiple results') {
+          searchResult.value = 'multiple'
+        } else {
+          sessionStorage.setItem('publicTrackingData', JSON.stringify(data.data))
+          router.push(`/public-tracking/detail?searchNo=${inputVModel.value}`)
+        }
+      }
+    })
+    .finally(() => {
+      loading.value = false
+    })
+  // router.push(`/public-tracking/detail?searchNo=${inputVModel.value}`)
 }
 </script>
 
 <template>
-  <div class="public-tracking-search">
+  <div class="public-tracking-search" v-vloading="loading">
     <div class="search-info">
       <div class="title">Tracking</div>
       <el-input
@@ -23,8 +67,8 @@ const handleSearchNo = () => {
         </template>
       </el-input>
       <div class="empty">
-        <VEmpty>
-          <template #default>
+        <VEmpty class="error-empty" v-if="searchResult === 'error'">
+          <template #suggestion>
             <div class="suggestion-info">
               <p>We support the following references number to find shipment:</p>
               <p>
@@ -34,12 +78,24 @@ const handleSearchNo = () => {
             </div>
           </template>
         </VEmpty>
+        <VEmpty class="multiple-empty" v-else-if="searchResult === 'multiple'">
+          <template #title> Sorry,Multiple results </template>
+          <template #result>
+            <p class="light">To correctly display the details page,</p>
+            <p class="light">please search using the HBL No. Thank you.</p>
+          </template>
+        </VEmpty>
       </div>
     </div>
   </div>
 </template>
 
 <style lang="scss" scoped>
+.light {
+  font-size: 12px;
+  line-height: 18px;
+  color: var(--color-neutral-3);
+}
 .public-tracking-search {
   display: flex;
   justify-content: center;
@@ -89,7 +145,8 @@ const handleSearchNo = () => {
       }
     }
   }
-  .empty {
+  .error-empty,
+  .multiple-empty {
     width: 480px;
     height: 315px;
     padding: 16px;
@@ -102,5 +159,9 @@ const handleSearchNo = () => {
       }
     }
   }
+  div.multiple-empty {
+    height: 236px;
+    padding-top: 24px;
+  }
 }
 </style>

+ 69 - 15
src/views/Tracking/src/components/PublicTracking/src/components/BasicInformation.vue

@@ -1,8 +1,7 @@
 <script setup lang="ts">
-import { useRouter } from 'vue-router'
-
-const router = useRouter()
-
+const props = defineProps({
+  data: Object
+})
 const allData: any = ref({
   basicInformation: {
     top: [
@@ -69,11 +68,71 @@ const allData: any = ref({
     }
   ]
 })
-
-// 跳转到shipment页面
-const handLink = (id: string) => {
-  router.push({ path: '/tracking', query: { id } })
+const convertData = (data: any) => {
+  return {
+    basicInformation: {
+      top: [
+        {
+          label: 'MAWB/MBL No.',
+          content: data.basicInfo['MAWB/MBL No.']
+        },
+        {
+          label: 'HAWB/HBL No.',
+          content: data.basicInfo['HAWB/HBOL']
+        },
+        {
+          label: 'Booking No.',
+          content: data.basicInfo.Carrier_Booking_No
+        },
+        {
+          label: 'PO No.',
+          content: data.basicInfo.PO_NO
+        }
+      ]
+    },
+    businessPartners: [
+      {
+        title: 'Origin Agent',
+        conpany: data.businessPartners.origin.company,
+        address: data.businessPartners.origin.address,
+        phone: data.businessPartners.origin.phone
+      },
+      {
+        title: 'Destination Agent',
+        conpany: data.businessPartners.destination.company,
+        address: data.businessPartners.destination.address,
+        phone: data.businessPartners.destination.phone
+      }
+    ],
+    packing: [
+      {
+        label: 'Quantity / Unit',
+        content: data.packing['Quantity/Unit']
+      },
+      {
+        label: 'G. Weight',
+        content: data.packing['G. Weight']
+      },
+      {
+        label: 'Ch. Weight',
+        content: data.packing['Ch. Weight']
+      },
+      {
+        label: 'Volume',
+        content: data.packing.Volume
+      }
+    ]
+  }
 }
+watch(
+  () => props.data,
+  (newVal) => {
+    if (newVal) {
+      allData.value = convertData(newVal)
+    }
+  },
+  { immediate: true, deep: true }
+)
 </script>
 
 <template>
@@ -83,15 +142,10 @@ const handLink = (id: string) => {
         <div class="title">
           <span>{{ item.label }}</span>
         </div>
-        <div class="content" @click="handLink(item.content)">
+        <div class="content">
           {{ item.content }}
         </div>
       </div>
-
-      <div v-for="item in allData.basicInformation.bottom" :key="item.label" class="data-item">
-        <div class="title">{{ item.label }}</div>
-        <div class="content">{{ item.content }}</div>
-      </div>
     </div>
     <div class="right-section">
       <div class="business-partner">
@@ -136,7 +190,7 @@ const handLink = (id: string) => {
   .left-section {
     display: flex;
     flex-direction: column;
-    gap: 18px;
+    gap: 20px;
     width: 334px;
     padding: 16px 16px 16px 0;
     border-right: 1px solid var(--color-border);

+ 52 - 36
src/views/Tracking/src/components/PublicTracking/src/components/MilestonesTable.vue

@@ -1,47 +1,23 @@
 <script setup lang="ts">
+import dayjs from 'dayjs'
+import timezone from 'dayjs/plugin/timezone'
+import utc from 'dayjs/plugin/utc'
 import { type VxeGridInstance, type VxeGridProps } from 'vxe-table'
+import { autoWidth } from '@/utils/table'
 import { useRowClickStyle } from '@/hooks/rowClickStyle'
 
+dayjs.extend(utc)
+dayjs.extend(timezone)
+const props = defineProps({
+  data: Object
+})
+
 const tableData = ref<VxeGridProps<any>>({
   border: true,
   minHeight: 70,
   maxHeight: 440,
-  columns: [
-    {
-      field: 'milestones',
-      title: 'Milestones',
-      minWidth: 120
-    },
-    {
-      field: 'dateTime',
-      title: 'Date Time',
-      minWidth: 80
-    },
-    {
-      field: 'locations',
-      title: 'Locations',
-      minWidth: 80
-    },
-    {
-      field: 'remarks',
-      title: 'Remarks',
-      minWidth: 80
-    }
-  ],
-  data: [
-    {
-      milestones: 'Milestone 1',
-      dateTime: 'Jun-08-2024 12:00 AM',
-      locations: 'Shenzhen',
-      remarks: 'Remarks 1'
-    },
-    {
-      milestones: 'Milestone 2',
-      dateTime: 'Jun-10-2024 12:00 AM',
-      locations: 'Valencia',
-      remarks: 'Remarks 2'
-    }
-  ],
+  columns: [],
+  data: [],
   scrollY: { enabled: true, oSize: 20, gt: 30 },
   stripe: true,
   emptyText: ' ',
@@ -54,6 +30,46 @@ const tableData = ref<VxeGridProps<any>>({
   rowConfig: { isHover: true }
 })
 
+const handleColumns = (columns: any) => {
+  const newColumns = columns.map((item: any) => {
+    let curColumn: any = {
+      title: item.title,
+      field: item.field
+    }
+
+    // 格式化
+    if (item.type === 'dateTime') {
+      curColumn = {
+        ...curColumn,
+        formatter: ({ cellValue, row }: any) => {
+          return cellValue
+            ? dayjs(cellValue).tz(row.timezone).format('MMM-DD-YYYY hh:mm A (z)')
+            : '--'
+        }
+      }
+    }
+    return curColumn
+  })
+  return newColumns
+}
+watch(
+  () => props.data,
+  (newVal) => {
+    const milestones = newVal?.Milestones
+    if (milestones && milestones.Milestones_column) {
+      tableData.value.columns = handleColumns(milestones.Milestones_column)
+      tableData.value.data = milestones.Milestones_data
+      nextTick(() => {
+        tableRef.value && autoWidth(tableData.value, tableRef.value)
+      })
+    }
+  },
+  {
+    immediate: true,
+    deep: true
+  }
+)
+
 const tableRef = ref<VxeGridInstance | null>(null)
 // 实现行点击样式
 useRowClickStyle(tableRef)

+ 83 - 11
src/views/Tracking/src/components/PublicTracking/src/components/PublicTrackingDetail.vue

@@ -1,22 +1,91 @@
 <script setup lang="ts">
 import BasicInformation from './BasicInformation.vue'
 import MilestonesTable from './MilestonesTable.vue'
+import { transportationMode } from '@/components/TransportationMode'
+import { useRoute } from 'vue-router'
+import dayjs from 'dayjs'
+
+const route = useRoute()
+
+const allData: any = ref({
+  transportInfo: {
+    'Tracking No.': '',
+    mode: '',
+    status: '',
+    origin: '',
+    destination: '',
+    etd: '',
+    atd: '',
+    eta: '',
+    ata: ''
+  },
+  basicInfo: {
+    'HAWB/HBOL': '',
+    Carrier_Booking_No: '',
+    PO_NO: ''
+  },
+  businessPartners: {
+    origin: {
+      company: '',
+      address: '',
+      phone: ''
+    },
+    destination: {
+      company: '',
+      address: '',
+      phone: ''
+    }
+  },
+  packing: {
+    'Quantity/Unit': '',
+    'G. Weight': '',
+    'Ch. Weight': '',
+    Volume: ''
+  }
+})
+const sharedData = JSON.parse(sessionStorage.getItem('publicTrackingData') || '{}')
+sessionStorage.removeItem('publicTrackingData')
+if (Object.keys(sharedData).length === 0) {
+  const loading = ref(false)
+  loading.value = true
+  $api
+    .getPublicTrackingDetail({ reference_number: route.query.searchNo })
+    .then((res) => {
+      if (res.code === 200) {
+        const { data } = res
+        allData.value = data.data
+      }
+    })
+    .finally(() => {
+      loading.value = false
+    })
+} else {
+  allData.value = sharedData
+}
+
+const formatTime = (time: string) => {
+  return time ? dayjs(time).format('MMM-DD-YYYY hh:mm A') : '--'
+}
 </script>
 
 <template>
   <div class="tracking-detail">
     <div class="header">
       <div class="detail-status">
-        <span class="font_family icon-icon_ocean_b" style="font-size: 64px"></span>
-        <div class="no">Tracking No. B83131200164</div>
-        <VTag large type="Confirmed">Confirmed</VTag>
+        <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>
       <div class="detail-info">
         <div class="item transport-way">
           <div class="place">
             <div class="title">Origin</div>
             <div class="content">
-              <span>Shenzhen</span>
+              <span>{{ allData.transportInfo?.origin }}</span>
               <div class="line_container">
                 <hr color="#000000" />
                 <div class="right-icon"></div>
@@ -25,21 +94,23 @@ import MilestonesTable from './MilestonesTable.vue'
           </div>
           <div class="place">
             <div class="title">Destination</div>
-            <div class="content">Valencia</div>
+            <div class="content">{{ allData.transportInfo?.destination }}</div>
           </div>
         </div>
         <div class="item">
           <div class="title">ETD/ATD</div>
           <div class="content">
-            <span>Jun-08-2024 12:00 AM / </span>
-            <span style="color: var(--color-neutral-1)">Jun-08-2024 12:00 AM</span>
+            <span>{{ formatTime(allData?.transportInfo?.etd) }} / </span>
+            <span style="color: var(--color-neutral-1)">{{
+              formatTime(allData?.transportInfo?.atd)
+            }}</span>
           </div>
         </div>
         <div class="item">
           <div class="title">ETA/ATA</div>
           <div class="content">
-            <span>Oct-26-2024 12:00 AM / </span>
-            <span>--</span>
+            <span>{{ formatTime(allData?.transportInfo?.eta) }} / </span>
+            <span>{{ formatTime(allData?.transportInfo?.ata) }}</span>
           </div>
         </div>
       </div>
@@ -55,14 +126,14 @@ import MilestonesTable from './MilestonesTable.vue'
           </div>
         </template>
         <template #content>
-          <BasicInformation ref="basicInformationRef"></BasicInformation>
+          <BasicInformation :data="allData" ref="basicInformationRef"></BasicInformation>
         </template>
       </VBox>
       <!-- Milestones -->
       <VBox style="margin-top: 16px" :isSeeAll="false" :is-draggable="false">
         <template #header>Milestones</template>
         <template #content>
-          <MilestonesTable></MilestonesTable>
+          <MilestonesTable :data="allData"></MilestonesTable>
         </template>
       </VBox>
     </div>
@@ -93,6 +164,7 @@ import MilestonesTable from './MilestonesTable.vue'
       position: relative;
       display: flex;
       align-items: center;
+      height: 64px;
       padding: 0 16px;
       border-bottom: 1px solid var(--color-border);
 

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

@@ -12,6 +12,9 @@ import AttachmentView from './components/AttachmentView.vue'
 import MapView from './components/MapView.vue'
 import { cloneDeep } from 'lodash'
 import { transportationMode } from '@/components/TransportationMode'
+import { useRoute } from 'vue-router'
+
+const route = useRoute()
 
 // 可拖拽模块的列表
 const boxList = ref([
@@ -75,9 +78,40 @@ const handleEmailDrawer = () => {
   emailDrawerRef.value.openDrawer(allData.value)
 }
 
+const amsIsfData = ref()
+const getAmsIsfData = () => {
+  $api
+    .getTrackingAmsIsf({
+      ams_ss: allData.value.ams_ss,
+      isf_ss: allData.value.isf_ss,
+      _schemas: allData.value._schemas
+    })
+    .then((res: any) => {
+      if (res.code === 200) {
+        // 获取数据
+        amsIsfData.value = res.data
+        isShowAMSISF.value = true
+      }
+    })
+}
+
 const AMSISFDrawerRef = ref()
+const isShowAMSISF = ref(false)
 const handleAMSISF = () => {
-  AMSISFDrawerRef.value.openDrawer()
+  if (isShowAMSISF.value) {
+    AMSISFDrawerRef.value?.openDrawer(amsIsfData.value)
+  } else {
+    // 如果 isShowAMSISF 从 false 变为 true,自动打开抽屉
+    watch(
+      isShowAMSISF,
+      (newValue) => {
+        if (newValue) {
+          AMSISFDrawerRef.value?.openDrawer(amsIsfData.value)
+        }
+      },
+      { immediate: true }
+    )
+  }
 }
 
 const allData = ref()
@@ -87,8 +121,8 @@ const getData = () => {
   $api
     .getTrackingDetail({
       status: 'Confirmed',
-      // a: 'UjwCcE8rEPXDQ6jYG3xp9V208RYJ6UrRpAH%2FRna8t%2BqjYnUcZnqOnvrE4Gg',
-      a: 'AjxXeBouEvrDQ6jYG3xp9V208RYJ6UrRpAH%2FRna8t%2BqjYnUcZnqOnvrE4Gg5'
+      a: route.query.a,
+      _schemas: route.query._schemas
     })
     .then((res: any) => {
       if (res.code === 200) {
@@ -98,6 +132,7 @@ const getData = () => {
     })
     .finally(() => {
       loading.value = false
+      getAmsIsfData()
     })
 }
 getData()
@@ -131,19 +166,30 @@ const formatTime = (time: string) => {
       </div>
       <div class="detail-info">
         <div class="item transport-way">
-          <div class="place">
+          <div class="origin">
             <div class="title">Origin</div>
             <div class="content">
-              <span>{{ allData?.transportInfo?.origin }}</span>
+              <el-tooltip placement="top">
+                <template #content>{{ allData?.transportInfo?.origin }}</template>
+                <span class="info single-line-ellipsis">{{ allData?.transportInfo?.origin }}</span>
+              </el-tooltip>
+
               <div class="line_container">
                 <hr color="#000000" />
                 <div class="right-icon"></div>
               </div>
             </div>
           </div>
-          <div class="place">
+          <div class="destination">
             <div class="title">Destination</div>
-            <div class="content">{{ allData?.transportInfo?.destination }}</div>
+            <div class="content">
+              <el-tooltip placement="top">
+                <template #content>{{ allData?.transportInfo?.destination }}</template>
+                <span class="info single-line-ellipsis">{{
+                  allData?.transportInfo?.destination
+                }}</span>
+              </el-tooltip>
+            </div>
           </div>
         </div>
         <div class="item">
@@ -165,7 +211,7 @@ const formatTime = (time: string) => {
       </div>
     </div>
     <div class="transport-map">
-      <MapView></MapView>
+      <MapView :serial_no="allData?.serial_no" :uncode="allData?.uncode"></MapView>
       <TransportStep class="transport-step" :data="allData"></TransportStep>
     </div>
     <div class="info-content">
@@ -330,8 +376,15 @@ const formatTime = (time: string) => {
         flex-direction: row;
         justify-content: flex-start;
         gap: 16px;
-        .place {
-          flex: 1;
+        max-width: 500px;
+        .origin {
+          width: 60%;
+        }
+        .destination {
+          width: 40%;
+          .info {
+            width: calc(100% - 16px);
+          }
         }
         .title {
           margin-top: 11px;
@@ -344,9 +397,12 @@ const formatTime = (time: string) => {
           font-size: 18px;
           font-weight: 700;
           color: var(--color-neutral-1);
+          width: 100%;
+
           .line_container {
             flex: 1;
             position: relative;
+            min-width: 26px;
             margin-left: 16px;
           }
           .line_container hr {
@@ -384,7 +440,6 @@ const formatTime = (time: string) => {
 }
 .fallback-class {
   opacity: 1 !important;
-  // background-color: #fff1e5 !important;
   cursor: move !important;
   background-color: #fff;
   box-shadow: 4px 4px 32px 0px rgba(0, 0, 0, 0.2);
@@ -401,6 +456,7 @@ const formatTime = (time: string) => {
   justify-content: flex-start;
   align-items: flex-start;
   gap: 8px;
+  min-height: 38px;
   .title {
     font-size: 12px;
     color: var(--color-neutral-2);
@@ -411,4 +467,11 @@ const formatTime = (time: string) => {
     color: var(--color-neutral-1);
   }
 }
+
+.single-line-ellipsis {
+  display: inline-block; /* 或者根据需要使用 inline-block */
+  white-space: nowrap; /* 不换行 */
+  overflow: hidden; /* 隐藏超出部分 */
+  text-overflow: ellipsis; /* 超出部分显示省略号 */
+}
 </style>

+ 75 - 79
src/views/Tracking/src/components/TrackingDetail/src/components/AMS&ISF.vue

@@ -3,53 +3,57 @@ import { type VxeGridInstance, type VxeGridProps } from 'vxe-table'
 import dayjs from 'dayjs'
 
 const drawer = ref(false)
-const openDrawer = () => {
+const openDrawer = (data: any) => {
+  getData(data)
   drawer.value = true
 }
 
+const canViewAMSLog = ref(false)
+const canViewISFLog = ref(false)
+const handleColumns = (columns: any) => {
+  const newColumns = columns.map((item: any) => {
+    let curColumn: any = {
+      title: item.title,
+      field: item.field
+    }
+
+    if (item.formatter === 'dateTime') {
+      curColumn = {
+        ...curColumn,
+        formatter: ({ cellValue }: any) => dayjs(cellValue).format('DD-MMM-YYYY HH:mm:ss') || '--'
+      }
+    }
+    return curColumn
+  })
+  return newColumns
+}
+const getData = (data: any) => {
+  const { amsLog, isfLog } = data
+  canViewAMSLog.value = data.canViewAMSLog
+  canViewISFLog.value = data.canViewISFLog
+  AMSTableData.value.columns = handleColumns(amsLog.amsLog_column)
+  AMSTableData.value.data = amsLog.data
+  ISFTableData.value.columns = handleColumns(isfLog.isfLog_column)
+  ISFTableData.value.data = data.isf
+}
+const drawerTitle = () => {
+  if (canViewAMSLog.value && canViewISFLog.value) {
+    return 'AMS/ISF'
+  } else if (canViewAMSLog.value) {
+    return 'AMS'
+  } else if (canViewISFLog.value) {
+    return 'ISF'
+  }
+}
+
 const AMSTableRef = ref<VxeGridInstance | null>(null)
 const AMSTableData = ref<VxeGridProps<any>>({
   border: true,
   round: true,
   minHeight: 70,
   maxHeight: 500,
-  columns: [
-    {
-      field: 'dateTime',
-      title: 'Date Time',
-      width: 217
-    },
-    {
-      field: 'code',
-      title: 'Code',
-      width: 185
-    },
-    {
-      field: 'name',
-      title: 'Name',
-      width: 230
-    },
-    {
-      field: 'description',
-      title: 'Description',
-      minWidth: 330
-    }
-  ],
-  data: [
-    {
-      dateTime: 'Jun-08-2024 12:00 AM',
-      code: 'Code 1',
-      name: 'Name 1',
-      description:
-        "A term commonly used in business transactions, particularly in the realm of recurring payments. It essentially means that a customer's payment information, such as a credit card number, expiration date, and CVV,"
-    },
-    {
-      dateTime: 'Jun-10-2024 12:00 AM',
-      code: 'Code 2',
-      name: 'Name 2',
-      description: 'Description 2'
-    }
-  ],
+  columns: [],
+  data: [],
   scrollY: { enabled: true, oSize: 20, gt: 30 },
   stripe: true,
   emptyText: ' ',
@@ -80,42 +84,8 @@ const ISFTableData = ref<VxeGridProps<any>>({
   round: true,
   minHeight: 70,
   maxHeight: 500,
-  columns: [
-    {
-      field: 'dateTime',
-      title: 'Date Time',
-      width: 217
-    },
-    {
-      field: 'code',
-      title: 'Code',
-      width: 185
-    },
-    {
-      field: 'name',
-      title: 'Name',
-      width: 230
-    },
-    {
-      field: 'description',
-      title: 'Description',
-      minWidth: 330
-    }
-  ],
-  data: [
-    {
-      milestones: 'Milestone 1',
-      dateTime: 'Jun-08-2024 12:00 AM',
-      locations: 'Shenzhen',
-      remarks: 'Remarks 1'
-    },
-    {
-      milestones: 'Milestone 2',
-      dateTime: 'Jun-10-2024 12:00 AM',
-      locations: 'Valencia',
-      remarks: 'Remarks 2'
-    }
-  ],
+  columns: [],
+  data: [],
   scrollY: { enabled: true, oSize: 20, gt: 30 },
   stripe: true,
   emptyText: ' ',
@@ -139,29 +109,55 @@ const exportISFTable = () => {
   })
 }
 
+const clearData = () => {
+  AMSTableData.value.data = []
+  AMSTableData.value.columns = []
+  ISFTableData.value.data = []
+  ISFTableData.value.columns = []
+}
+
 defineExpose({
   openDrawer
 })
 </script>
 
 <template>
-  <el-drawer :modal="false" :size="1000" v-model="drawer" title="AMS/ISF" direction="rtl">
+  <el-drawer
+    :modal="false"
+    @close="clearData"
+    :size="1000"
+    v-model="drawer"
+    :title="drawerTitle()"
+    direction="rtl"
+  >
     <div class="ams-isf">
-      <div class="label" style="margin-top: 8px">
+      <div class="label" v-if="canViewAMSLog" style="margin-top: 8px">
         <span>AMS-M1 Log</span>
         <el-button class="el-button--icon" @click="exportAMSTable">
           <span class="font_family icon-icon_export_b"></span>
         </el-button>
       </div>
-      <vxe-grid ref="AMSTableRef" class="radius-bottom" :style="{ border: 'none' }" v-bind="AMSTableData">
+      <vxe-grid
+        v-if="canViewAMSLog"
+        ref="AMSTableRef"
+        class="radius-bottom"
+        :style="{ border: 'none' }"
+        v-bind="AMSTableData"
+      >
       </vxe-grid>
-      <div class="label">
+      <div class="label" v-if="canViewISFLog">
         <span>ISF Log</span>
         <el-button class="el-button--icon" @click="exportISFTable">
           <span class="font_family icon-icon_export_b"></span>
         </el-button>
       </div>
-      <vxe-grid class="radius-bottom" ref="ISFTableRef" :style="{ border: 'none' }" v-bind="ISFTableData">
+      <vxe-grid
+        v-if="canViewISFLog"
+        class="radius-bottom"
+        ref="ISFTableRef"
+        :style="{ border: 'none' }"
+        v-bind="ISFTableData"
+      >
       </vxe-grid>
     </div>
   </el-drawer>

+ 4 - 20
src/views/Tracking/src/components/TrackingDetail/src/components/AttachmentView.vue

@@ -61,22 +61,6 @@ const handleColumns = (columns: any) => {
         slots: { default: 'file' }
       }
     }
-
-    // 格式化
-    if (item.formatter === 'date') {
-      curColumn = {
-        ...curColumn,
-        sortBy: ({ row, column }: any) => {
-          return dayjs(row[column.field]).unix()
-        },
-        formatter: ({ cellValue }: any) => dayjs(cellValue).format('MMM-YYYY-DD ') || '--'
-      }
-    } else if (item.formatter === 'dateTime') {
-      curColumn = {
-        ...curColumn,
-        formatter: ({ cellValue }: any) => dayjs(cellValue).format('MMM-YYYY-DD HH:mm:ss') || '--'
-      }
-    }
     return curColumn
   })
   return newColumns
@@ -137,18 +121,18 @@ const openUploadFilesDialog = () => {
 
 <template>
   <div class="attachment">
-    <el-button @click="openUploadFilesDialog" class="el-button--text title">
+    <!-- <el-button @click="openUploadFilesDialog" class="el-button--text title">
       <span class="font_family icon-icon_upload_b"></span>
       Upload Files
-    </el-button>
+    </el-button> -->
     <vxe-grid class="radius-bottom" ref="tableRef" v-bind="tableData">
       <template #action="{ row }">
         <el-button @click="handleDownload(row)" class="el-button--icon">
           <span class="font_family icon-icon_download_b"></span>
         </el-button>
-        <el-button @click="handleDelete(row)" class="el-button--icon">
+        <!-- <el-button @click="handleDelete(row)" class="el-button--icon">
           <span class="font_family icon-icon_delete_b"></span>
-        </el-button>
+        </el-button> -->
       </template>
       <template #file="{ row, column }">
         <div class="file" v-if="row[column.field]?.file_name">

+ 8 - 6
src/views/Tracking/src/components/TrackingDetail/src/components/BasicInformation.vue

@@ -215,12 +215,14 @@ const handLink = (id: string) => {
 
 // 复制文本
 const handleCopy = (data: any) => {
-  const sanitizedData = data.map((item: string) =>
-    item
-      .replace(/[\r\n]+/g, ' ')
-      .replace(/\s+/g, ' ')
-      .trim()
-  ) // 用空格替换换行符
+  const sanitizedData = data.map((item: string) => {
+    return item
+      ? item
+          .replace(/[\r\n]+/g, ' ')
+          .replace(/\s+/g, ' ')
+          .trim()
+      : ''
+  }) // 用空格替换换行符
   const copyText = sanitizedData.join('\n') // 拼接为单行文本
   if (XEClipboard.copy(copyText)) {
     ElMessage.success('Copy success')

+ 4 - 2
src/views/Tracking/src/components/TrackingDetail/src/components/ContainersView.vue

@@ -41,12 +41,14 @@ const handleColumns = (columns: any) => {
         sortBy: ({ row, column }: any) => {
           return dayjs(row[column.field]).unix()
         },
-        formatter: ({ cellValue }: any) => dayjs(cellValue).format('MMM-YYYY-DD ') || '--'
+        formatter: ({ cellValue }: any) =>
+          cellValue ? dayjs(cellValue).format('MMM-YYYY-DD ') : '--'
       }
     } else if (item.formatter === 'dateTime') {
       curColumn = {
         ...curColumn,
-        formatter: ({ cellValue }: any) => dayjs(cellValue).format('MMM-YYYY-DD HH:mm:ss') || '--'
+        formatter: ({ cellValue }: any) =>
+          cellValue ? dayjs(cellValue).format('MMM-YYYY-DD HH:mm:ss') : '--'
       }
     }
     return curColumn

+ 187 - 30
src/views/Tracking/src/components/TrackingDetail/src/components/MapView.vue

@@ -1,52 +1,184 @@
-<!-- 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'
+import DestinationIcon from '../images/destinationIcon.png'
+import OriginIcon from '../images/originIcon.png'
+import TransferIcon from '../images/transferIcon.png'
+import { onMounted, ref, watch } from 'vue'
 
+const props = defineProps<{
+  serial_no: string
+  uncoded: string
+}>()
+
+const markerPositions = ref([])
+// 创建自定义图标
+const originIcon = L.icon({
+  iconUrl: OriginIcon,
+  iconSize: [20, 20],
+  iconAnchor: [10, 20],
+  popupAnchor: [0, -8]
+})
+
+const destinationIcon = L.icon({
+  iconUrl: DestinationIcon,
+  iconSize: [20, 20],
+  iconAnchor: [10, 20],
+  popupAnchor: [0, -8]
+})
+
+const transferIcon = L.icon({
+  iconUrl: TransferIcon,
+  iconSize: [20, 20],
+  iconAnchor: [10, 20],
+  popupAnchor: [0, -8]
+})
+
+let map: L.Map | null = null
+
+// 定义响应式的重置缩放中心和级别
+const resetZoomCenter: any = ref([51.505, -0.09]) // 默认中心
+const resetZoomLevel = ref(5) // 默认缩放级别
+
+// 初始化地图(不添加标记)
 const initMap = () => {
-  // 地图初始化
-  const map = L.map('map').setView([51.505, -0.09], 3)
+  if (map) {
+    return
+  }
+
+  map = L.map('map').setView([51.505, -0.09], 3)
 
   // 添加 TileLayer
-  L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
+  L.tileLayer('https://map.kerryapex.com/osm_tiles/{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 addResetZoomButton = (center: L.LatLng, zoom: number) => {
+  const ResetZoomControl = L.Control.extend({
+    options: {
+      position: 'topleft'
+    },
+    onAdd: function () {
+      const container = L.DomUtil.create('div', 'reset-zoom-control leaflet-bar')
+
+      // 创建重置缩放按钮
+      const resetZoomButton = L.DomUtil.create('a', 'reset-zoom-button leaflet-bar-part', container)
+      resetZoomButton.href = '#'
+      resetZoomButton.title = 'Reset Zoom'
+      resetZoomButton.innerHTML = `<div class="outer-ring" style="height: 100%; padding: 4px;border: 1px solid #2b2f36;border-radius: 50%;">
+        <div class="inner-ring" style=" height: 100%; width: 100%; background-color: #2b2f36; border-radius: 50%;"></div>
+        </div>`
+      resetZoomButton.setAttribute('role', 'button')
 
-  const customIcon = L.icon({
-    iconUrl: Location,
-    iconSize: [20, 20], // 图标尺寸
-    iconAnchor: [10, 20], // 图标锚点
-    popupAnchor: [0, -8] // 弹出层位置
+      // 点击按钮时重置地图到首次标记后的缩放和中心
+      resetZoomButton.addEventListener('click', () => {
+        map!.setView(center, zoom)
+      })
+
+      return container
+    }
   })
 
-  const marker = L.marker([51.5, -0.09], { icon: customIcon }).addTo(map)
+  // 添加重置按钮到地图
+  new ResetZoomControl().addTo(map!)
+}
+
+// 定义首次渲染地图的中心和缩放级别
+let initialCenter: L.LatLng | null = null
+let initialZoomLevel: number | null = null
+let isFirstRender = true // 标记是否为首次渲染
 
-  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>
-  `
+// 添加标记后更新中心和缩放级别
+const addMarkersToMap = () => {
+  if (!map) return // 确保地图已经初始化
+  const latLngBounds: any = [] // 用来存储所有标记的坐标
+  markerPositions.value.forEach((position) => {
+    const marker = L.marker([position.lat, position.lng], { icon: position.icon }).addTo(map)
 
-  // 绑定弹出框并立即展示
-  marker.bindPopup(customPopupContent, popupOptions).openPopup()
+    const customPopupContent = `
+      <div class="popup-content">
+        <p class="label">${position.label}</p>
+        <p>
+          <span class="font_family icon-icon_location_fill_b" style="color: ${position.iconColor}"></span>
+          <span>${position.city}</span>
+        </p>
+      </div>
+    `
+    marker
+      .bindPopup(customPopupContent, {
+        closeButton: false,
+        autoClose: false,
+        closeOnClick: false
+      })
+      .openPopup()
+    latLngBounds.push([position.lat, position.lng])
+  })
+
+  if (latLngBounds.length > 0) {
+    const bounds = L.latLngBounds(latLngBounds)
+    map!.fitBounds(bounds, { paddingTopLeft: [20, 20], paddingBottomRight: [400, 40] })
+
+    // 首次添加标记时保存中心和缩放级别
+    if (isFirstRender) {
+      initialCenter = bounds.getCenter() // 保存中心
+      initialZoomLevel = map!.getZoom() // 保存缩放比例
+      isFirstRender = false
+
+      // 接口请求成功并首次添加标记后,动态添加重置按钮
+      addResetZoomButton(initialCenter, initialZoomLevel)
+    }
+  }
+}
+
+// 请求接口并处理标记
+const getMarker = () => {
+  $api
+    .getTrackingDetailMapData({
+      serial_no: props.serial_no,
+      uncoded: props.uncoded
+    })
+    .then((res) => {
+      if (res.code === 200) {
+        const { data } = res
+        data.forEach((item) => {
+          const iconColorList = {
+            Destination: { color: '#24ca5a', icon: destinationIcon },
+            Origin: { color: '#ED6D00', icon: originIcon },
+            Transfer: { color: '#ed0000', icon: transferIcon }
+          }
+          markerPositions.value.push({
+            lat: item.lat,
+            lng: item.lng,
+            city: item.infor,
+            label: item.label,
+            icon: iconColorList[item.label].icon,
+            iconColor: iconColorList[item.label].color
+          })
+        })
+        // 请求成功后添加标记,并动态添加重置按钮
+        addMarkersToMap()
+      }
+    })
 }
 
+// 监听 `serial_no` 变化
+watch(
+  () => props.serial_no,
+  (val) => {
+    if (val) {
+      getMarker()
+    }
+  },
+  { immediate: true }
+)
+
+// 当组件挂载时初始化地图
 onMounted(() => {
-  initMap()
+  initMap() // 初始化地图,不加标记
 })
 </script>
 
@@ -57,10 +189,12 @@ onMounted(() => {
 }
 .leaflet-popup-content {
   padding: 4px;
+  padding-right: 8px;
   margin: 0;
   .popup-content {
     p {
       margin: 0;
+      white-space: nowrap;
       span {
         margin-left: -2px;
         font-size: 12px;
@@ -75,10 +209,33 @@ onMounted(() => {
   }
 }
 
-/* 自定义弹出窗口箭头 */
+/* 自定义重置缩放按钮控件样式 */
+.reset-zoom-control {
+  margin-top: 10px; /* 增加上边距,使按钮与默认缩放按钮之间有间距 */
+  border-radius: 4px;
+  background-color: white;
+  border: 1px solid #ccc;
+  box-shadow: 0 1px 5px rgba(0, 0, 0, 0.3);
+}
+
+.reset-zoom-button {
+  background-color: white;
+  border: none;
+  cursor: pointer;
+  padding: 6px;
+}
+
+/* 隐藏默认的弹出窗口箭头 */
 .leaflet-popup-tip {
   display: none;
 }
-.popup-content {
+.transport-map {
+  .leaflet-touch {
+    .leaflet-bar {
+      border: 0;
+      border-radius: 4px;
+      overflow: hidden;
+    }
+  }
 }
 </style>

+ 6 - 10
src/views/Tracking/src/components/TrackingDetail/src/components/MilestonesTable.vue

@@ -71,18 +71,14 @@ const handleColumns = (columns: any) => {
     }
 
     // 格式化
-    if (item.formatter === 'date') {
+    if (item.formatter === 'dateTime') {
       curColumn = {
         ...curColumn,
-        sortBy: ({ row, column }: any) => {
-          return dayjs(row[column.field]).unix()
-        },
-        formatter: ({ cellValue }: any) => dayjs(cellValue).format('MMM-YYYY-DD ') || '--'
-      }
-    } else if (item.formatter === 'dateTime') {
-      curColumn = {
-        ...curColumn,
-        formatter: ({ cellValue }: any) => dayjs(cellValue).format('MMM-YYYY-DD HH:mm:ss') || '--'
+        formatter: ({ cellValue, row }: any) => {
+          return cellValue
+            ? dayjs(cellValue).tz(row.timezone).format('MMM-DD-YYYY hh:mm A (z)')
+            : '--'
+        }
       }
     }
     return curColumn

+ 164 - 89
src/views/Tracking/src/components/TrackingDetail/src/components/RoutesView.vue

@@ -7,8 +7,8 @@ const routes: any = ref([
   {
     serialNumber: 'Leg 1',
     mode: 'Sea',
-    origin: 'ShenzhenShwe',
-    destination: 'Valenciaenzq',
+    origin: 'ShenzhenShw',
+    destination: 'ShenzhenS',
     etd: 'Jun-08-2024 12:00 AM',
     atd: 'Jun-10-2024 12:00 AM',
     eta: 'Jun-14-2024 12:00 AM',
@@ -21,7 +21,7 @@ const routes: any = ref([
     serialNumber: 'Leg 2',
     mode: 'Sea',
     origin: 'Valenciaenz',
-    destination: 'New York',
+    destination: 'ShenzhenShweShenzhenShweShenzhenShweShenzhenShwe',
     etd: 'Jun-15-2024 12:00 AM',
     atd: 'Jun-17-2024 12:00 AM',
     eta: 'Jun-21-2024 12:00 AM',
@@ -50,10 +50,40 @@ watch(
 const formatDate = (date: string) => {
   return date ? dayjs(date).format('MMM-DD-YYYY HH:mm A') : '--'
 }
+
+const basicDesWidth = ref('180px')
+const detailDesWidth = ref('220px')
+
+const getContainerWidth = () => {
+  let screenWidth = document.body.clientWidth
+  if (screenWidth < 1300) {
+    basicDesWidth.value = '180px'
+    detailDesWidth.value = '220px'
+  } else if (screenWidth < 1500) {
+    basicDesWidth.value = '220px'
+    detailDesWidth.value = '240px'
+  } else {
+    basicDesWidth.value = '240px'
+    detailDesWidth.value = '260px'
+  }
+}
+onMounted(() => {
+  getContainerWidth()
+  window.addEventListener('resize', getContainerWidth)
+})
+onBeforeUnmount(() => {
+  window.removeEventListener('resize', getContainerWidth)
+})
 </script>
 
 <template>
-  <div class="routes-view">
+  <div
+    class="routes-view"
+    :style="{
+      '--basic-destination-width': basicDesWidth,
+      '--detail-destination-width': detailDesWidth
+    }"
+  >
     <div class="title">Total number of legs: {{ routes.length }}</div>
     <div class="routes">
       <div class="route-item" v-for="(item, index) in routes" :key="item.serialNumber">
@@ -67,16 +97,23 @@ const formatDate = (date: string) => {
             <div class="origin">
               <div class="title">Origin</div>
               <div class="content">
-                <span>{{ item.origin }}</span>
+                <el-tooltip placement="top">
+                  <template #content>{{ item.origin }}</template>
+                  <span class="single-line-ellipsis">{{ item.origin }}</span>
+                </el-tooltip>
+
                 <div class="line_container">
                   <hr color="#000000" />
                   <div class="right-icon"></div>
                 </div>
               </div>
             </div>
-            <div class="destination">
+            <div class="destination" ref="">
               <div class="title">Destination</div>
-              <div class="content">{{ item.destination }}</div>
+              <el-tooltip placement="top">
+                <template #content>{{ item.destination }}</template>
+                <div class="content single-line-ellipsis">{{ item.destination }}</div>
+              </el-tooltip>
             </div>
           </div>
           <div class="etd border-right">
@@ -98,7 +135,11 @@ const formatDate = (date: string) => {
             <div class="origin">
               <div class="place">
                 <span style="font-size: 24px" class="font_family icon-icon_location_fill_b"></span>
-                <span class="label">{{ item.origin }}</span>
+
+                <el-tooltip placement="top">
+                  <template #content>{{ item.origin }}</template>
+                  <span class="label single-line-ellipsis">{{ item.origin }}</span>
+                </el-tooltip>
                 <div class="line_container">
                   <hr color="#000000" />
                   <div class="right-icon"></div>
@@ -118,7 +159,11 @@ const formatDate = (date: string) => {
             <div class="destination">
               <div class="place">
                 <span style="font-size: 24px" class="font_family icon-icon_location_fill_b"></span>
-                <span class="label">{{ item.destination }}</span>
+
+                <el-tooltip placement="top">
+                  <template #content>{{ item.destination }}</template>
+                  <span class="label single-line-ellipsis">{{ item.destination }}</span>
+                </el-tooltip>
               </div>
               <div class="eta">
                 <span class="font_family icon-icon_date_b"></span>
@@ -192,31 +237,38 @@ const formatDate = (date: string) => {
       font-size: 40px;
     }
   }
-  .origin {
-    flex: 1;
-  }
 
   .destination {
-    flex-basis: 100px;
+    flex: 1;
+    max-width: calc(var(--basic-destination-width) - 16px);
     display: flex;
     flex-direction: column;
     justify-content: space-between;
     margin-left: 16px;
+    & > div.content {
+      display: inline-block;
+      width: 100%;
+    }
   }
   .place {
-    flex: 1 1 140px;
+    flex: 0 0 40%;
     display: flex;
     align-items: center;
     padding: 0 16px;
+    overflow: hidden;
 
     .title {
       margin-top: 2px;
     }
+    .origin {
+      width: calc(100% - var(--basic-destination-width));
+    }
 
     .content {
       position: relative;
       display: flex;
       align-items: center;
+      width: 100%;
       margin-top: 8px;
       font-size: 18px;
       font-weight: 700;
@@ -235,6 +287,7 @@ const formatDate = (date: string) => {
       font-size: 20px;
       transition: all 0.3s;
       transform: rotate(0deg);
+      cursor: pointer;
     }
 
     .collapse {
@@ -251,7 +304,12 @@ const formatDate = (date: string) => {
     gap: 8px;
     padding: 16px;
   }
-
+  .etd {
+    min-width: 180px;
+  }
+  .eta {
+    min-width: 220px;
+  }
   .place,
   .etd,
   .eta {
@@ -271,7 +329,9 @@ const formatDate = (date: string) => {
   gap: 8px;
   padding: 8px;
   border-top: 1px solid var(--color-border);
-
+  .line_container {
+    flex: 1;
+  }
   .line_container hr {
     border-color: #484c52;
     border-top: none;
@@ -286,94 +346,101 @@ const formatDate = (date: string) => {
     width: 10px;
     height: 12px;
   }
-}
-
-.date-info {
-  flex: 1;
-  display: flex;
-  justify-content: space-between;
-  gap: 16px;
-  padding: 12px 8px 12px;
-  background-color: var(--color-header-bg);
-  border-radius: 6px;
-
-  .origin {
-    flex: 1;
+  .date-info {
     display: flex;
-    flex-direction: column;
     justify-content: space-between;
-  }
-
-  .destination {
-    flex-basis: 200px;
-    display: flex;
-    flex-direction: column;
-    justify-content: space-between;
-    margin-left: 16px;
-  }
-
-  .place {
-    display: flex;
-    align-items: center;
-    gap: 8px;
+    gap: 16px;
+    width: calc(100% - 248px);
+    padding: 12px 8px 12px;
+    background-color: var(--color-header-bg);
+    border-radius: 6px;
+
+    .origin {
+      flex: 1;
+      display: flex;
+      flex-direction: column;
+      justify-content: space-between;
 
-    .label {
-      font-size: 18px;
-      font-weight: 700;
+      min-width: var(--detail-destination-width);
+      max-width: calc(100% - var(--detail-destination-width));
     }
 
-    .content {
-      position: relative;
+    .destination {
+      flex: 1;
+      min-width: calc(var(--detail-destination-width) - 16px);
       display: flex;
-      align-items: center;
-      margin-top: 8px;
-      font-size: 18px;
-      font-weight: 700;
-      color: var(--color-neutral-1);
-    }
-  }
-
-  .etd,
-  .eta,
-  .atd,
-  .ata {
-    display: flex;
-    align-items: center;
-    margin-left: 5px;
-    font-size: 12px;
-    line-height: 16px;
-    color: var(--color-neutral-2);
-
-    .font_family {
-      margin-right: 12px;
+      flex-direction: column;
+      justify-content: space-between;
+      .label {
+        width: calc(100% - 40px);
+      }
     }
 
-    .value {
-      margin-left: 4px;
-      font-weight: 700;
+    .place {
+      width: 100%;
+      display: flex;
+      align-items: center;
+      gap: 8px;
+
+      .label {
+        display: inline-block;
+        font-size: 18px;
+        font-weight: 700;
+      }
+
+      .content {
+        position: relative;
+        display: flex;
+        align-items: center;
+        margin-top: 8px;
+        font-size: 18px;
+        font-weight: 700;
+        color: var(--color-neutral-1);
+      }
     }
-  }
-}
 
-.transport-info {
-  width: 288px;
-  padding: 8px 16px;
-  background-color: var(--color-header-bg);
-  border-radius: 6px;
-
-  .item {
-    .title {
-      margin-bottom: 4px;
+    .etd,
+    .eta,
+    .atd,
+    .ata {
+      display: flex;
+      align-items: center;
+      margin-left: 5px;
       font-size: 12px;
+      line-height: 16px;
       color: var(--color-neutral-2);
-    }
 
-    .content {
-      font-weight: 700;
+      .font_family {
+        margin-right: 12px;
+      }
+
+      .value {
+        margin-left: 4px;
+        font-weight: 700;
+      }
     }
+  }
 
-    & + .item {
-      margin-top: 16px;
+  .transport-info {
+    width: 248px;
+    padding: 8px 16px;
+    background-color: var(--color-header-bg);
+    border-radius: 6px;
+
+    .item {
+      .title {
+        margin-bottom: 4px;
+        font-size: 12px;
+        color: var(--color-neutral-2);
+      }
+
+      .content {
+        font-weight: 700;
+      }
+
+      & + .item {
+        margin-top: 16px;
+      }
     }
   }
 }
@@ -384,6 +451,7 @@ const formatDate = (date: string) => {
 
 .line_container {
   flex: 1;
+  min-width: 26px;
   position: relative;
   margin-left: 16px;
 }
@@ -404,4 +472,11 @@ const formatDate = (date: string) => {
   transform: rotate(30deg);
   border-radius: 0 1px 0 0;
 }
+
+.single-line-ellipsis {
+  display: inline-block; /* 或者根据需要使用 inline-block */
+  white-space: nowrap; /* 不换行 */
+  overflow: hidden; /* 隐藏超出部分 */
+  text-overflow: ellipsis; /* 超出部分显示省略号 */
+}
 </style>

+ 15 - 5
src/views/Tracking/src/components/TrackingDetail/src/components/UploadFilesDialog.vue

@@ -56,11 +56,17 @@ const clearData = () => {
   uploadFileList.value = []
 }
 const beforeAvatarUpload = (rawFile: any) => {
-  if (!['application/pdf', '.docx', '.xlsx'].includes(rawFile.type)) {
-    ElMessage.error('Avatar picture must be JPG format!')
+  if (
+    ![
+      'application/pdf',
+      'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
+      'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
+    ].includes(rawFile.type)
+  ) {
+    ElMessage.error('The file types allowed for upload are: PDF, DOCX, and XLSX.')
     return false
   } else if (rawFile.size / 1024 / 1024 > 25) {
-    ElMessage.error('Avatar picture size can not exceed 25MB!')
+    ElMessage.error('File size must not exceed 25MB!')
     return false
   }
   return true
@@ -84,7 +90,6 @@ const beforeAvatarUpload = (rawFile: any) => {
         class="upload-demo"
         ref="uploadRef"
         drag
-        :auto-upload="false"
         :accept="'application/pdf,.docx,.xlsx'"
         :show-file-list="false"
         :action="'http://localhost:3000/upload'"
@@ -134,7 +139,12 @@ const beforeAvatarUpload = (rawFile: any) => {
     </el-scrollbar>
     <template #footer>
       <div class="dialog-footer">
-        <el-button class="download-btn el-button--dark" @click="handleSave">Finish</el-button>
+        <el-button
+          style="height: 40px; padding: 8px 40px"
+          class="download-btn el-button--dark"
+          @click="handleSave"
+          >Finish</el-button
+        >
       </div>
     </template>
   </el-dialog>

+ 0 - 0
src/views/Tracking/src/components/TrackingDetail/src/images/location.png → src/views/Tracking/src/components/TrackingDetail/src/images/destinationIcon.png


BIN
src/views/Tracking/src/components/TrackingDetail/src/images/originIcon.png


BIN
src/views/Tracking/src/components/TrackingDetail/src/images/transferIcon.png


+ 74 - 20
src/views/Tracking/src/components/TrackingTable/src/TrackingTable.vue

@@ -1,13 +1,15 @@
 <script setup lang="ts">
-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 dayjs from 'dayjs'
 import { useRouter } from 'vue-router'
 import { transportationMode } from '@/components/TransportationMode'
+import { useHeaderSearch } from '@/stores/modules/headerSearch'
+import { fa } from 'element-plus/es/locales.mjs'
 
 const router = useRouter()
+const headerSearch = useHeaderSearch()
 const props = defineProps({
   height: {
     type: Number,
@@ -52,12 +54,14 @@ const handleColumns = (columns: any, status?: string) => {
         sortBy: ({ row, column }: any) => {
           return dayjs(row[column.field]).unix()
         },
-        formatter: ({ cellValue }: any) => dayjs(cellValue).format('MMM-YYYY-DD ') || '--'
+        formatter: ({ cellValue }: any) =>
+          cellValue ? dayjs(cellValue).format('MMM-YYYY-DD ') : '--'
       }
     } else if (item.formatter === 'dateTime') {
       curColumn = {
         ...curColumn,
-        formatter: ({ cellValue }: any) => dayjs(cellValue).format('MMM-YYYY-DD HH:mm:ss') || '--'
+        formatter: ({ cellValue }: any) =>
+          cellValue ? dayjs(cellValue).format('MMM-YYYY-DD HH:mm:ss') : '--'
       }
     }
     return curColumn
@@ -72,7 +76,8 @@ const getTableColumns = async (isInit: boolean) => {
     if (res.code === 200) {
       trackingTable.value.columns = [
         { type: 'checkbox', width: 50, fixed: 'left' },
-        ...handleColumns(res.data.TrackingTableColumns)
+        ...handleColumns(res.data.TrackingTableColumns),
+        { title: 'Action', fixed: 'right', width: 80, slots: { default: 'action' } }
       ]
       tableOriginColumnsField.value = res.data.TrackingTableColumns
     }
@@ -88,7 +93,32 @@ const TagsList = ref()
 
 // 获取表格数据
 let filterdataobj: any = {}
+const getSharedTableData = () => {
+  const trackingData = JSON.parse(localStorage.getItem('TrackingData'))
+  if (trackingData) {
+    trackingTable.value.data = trackingData.searchData
+    pageInfo.value.total = Number(trackingData.rc)
+    TransportListItem.value = trackingData.TransportList
+    TagsList.value = trackingData.tagsList
+
+    // 拥有所有字段的表格
+    setTimeout(() => {
+      allTable.value.columns = handleColumns(trackingData.allColums, 'all')
+      allTable.value.data = trackingData.searchData
+      nextTick(() => {
+        allTableRef.value && autoWidth(allTable.value, allTableRef.value)
+      })
+    }, 1000)
+    localStorage.removeItem('TrackingData')
+    return true
+  }
+  return false
+}
+
 const getTableData = async (isInit: boolean, isPageChange?: boolean) => {
+  if (getSharedTableData()) {
+    return
+  }
   const rc = isPageChange ? pageInfo.value.total : -1
   tableLoading.value = true
   if (
@@ -143,10 +173,21 @@ const getTableData = async (isInit: boolean, isPageChange?: boolean) => {
     !isInit && (tableLoading.value = false)
   })
 }
+
+// 当 sharedData 发生变化时,更新 inputValue
+watch(
+  () => headerSearch.isChangeByLogin,
+  (newData) => {
+    if (newData) {
+      getSharedTableData()
+      headerSearch.clearChangeByLogin()
+    }
+  }
+)
+
 // 查询列表数据
 const searchTableData = (data: any) => {
   tableLoading.value = true
-  console.log(data)
   filterdataobj = data
   $api
     .getTrackingTableData({
@@ -162,7 +203,10 @@ const searchTableData = (data: any) => {
         if (res.data.searchData.length == 1) {
           router.push({
             path: '/tracking/detail',
-            query: { a: res.data.searchData.__serial_no, _schemas: res.data.searchData.__schemas }
+            query: {
+              a: res.data.searchData[0].__serial_no,
+              _schemas: res.data.searchData[0].__schemas
+            }
           })
         } else {
           trackingTable.value.data = res.data.searchData
@@ -183,8 +227,8 @@ onMounted(() => {
   })
 })
 
-const tableRef = ref<VxeGridInstance>()
-const trackingTable = ref<VxeGridProps<any>>({
+const tableRef = ref<any>()
+const trackingTable = ref<any>({
   border: true,
   round: true,
   columns: [],
@@ -206,8 +250,8 @@ const trackingTable = ref<VxeGridProps<any>>({
   }
 })
 
-const allTableRef = ref<VxeGridInstance>()
-const allTable = ref<VxeGridProps<any>>({
+const allTableRef = ref<any>()
+const allTable = ref<any>({
   columns: [],
   data: [],
   showHeaderOverflow: true,
@@ -258,7 +302,7 @@ const exportTable = (status: number) => {
   }
 }
 
-const tableLoading = ref(true)
+const tableLoading = ref(false)
 
 const CustomizeColumnsRef = ref()
 // 打开定制表格弹窗
@@ -292,17 +336,17 @@ const handleCellDblclick = ({ row }: any) => {
     query: { a: row.__serial_no, _schemas: row._schemas }
   })
 }
-// 点击link字段
+// 点击link字段时
 const handleLinkClick = (row: any, column: any) => {
-  if (column.field === 'booking_no') {
+  if (column.title === 'Booking No.') {
     router.push({
       path: '/booking/detail',
       query: { a: row.__serial_no, _schemas: row._schemas }
     })
-  } else if (column.field === 'h_bol') {
+  } else if (column.title === 'HBL No.') {
     router.push({
       path: '/tracking/detail',
-      query: { a: row.__serial_no, _schemas: row._schemas }
+      query: { a: row.__serial_no, _schemas: row._schemas, serial_no: row.serial_no }
     })
   }
 }
@@ -315,6 +359,14 @@ const handleCheckboxChange = ({ records }: any) => {
 const handleCheckAllChange = ({ records }: any) => {
   selectedNumber.value = records.length
 }
+
+// VGM
+const handleVGM = (row) => {
+  router.push({
+    path: '/tracking/add-vgm',
+    query: { a: row.__serial_no, _schemas: row._schemas }
+  })
+}
 defineExpose({
   searchTableData,
   TransportListItem,
@@ -351,7 +403,7 @@ defineExpose({
       <!-- 空数据时的插槽 -->
       <template #empty>
         <VEmpty>
-          <template #default>
+          <template #suggestion>
             <p>We support the following references number to find tracking:</p>
             <p>
               · Tracking No./HAWB No./MAWB No./PO No./Carrier Tracking No./Contract No./File
@@ -361,8 +413,8 @@ defineExpose({
         </VEmpty>
       </template>
       <!-- action操作的插槽 -->
-      <template #action>
-        <el-button class="el-button--blue" style="height: 24px">
+      <template #action="{ row }">
+        <el-button @click="handleVGM(row)" class="el-button--blue" style="height: 24px">
           <span class="font_family icon-icon_vgm_b"></span> <span style="font-size: 12px">VGM</span>
         </el-button>
       </template>
@@ -392,7 +444,7 @@ defineExpose({
     </vxe-grid>
     <vxe-grid :height="10" ref="allTableRef" class="all-table" v-bind="allTable"> </vxe-grid>
     <div class="bottom-pagination">
-      <div class="left-total-records">Total 181</div>
+      <div class="left-total-records">Total {{ pageInfo.total }}</div>
       <div class="right-pagination">
         <el-pagination
           v-model:current-page="pageInfo.pageNo"
@@ -402,6 +454,8 @@ defineExpose({
           background
           layout="sizes, prev, pager, next"
           :total="pageInfo.total"
+          @size-change="getTableData(false, true)"
+          @current-change="getTableData(false, true)"
         />
       </div>
     </div>
@@ -456,4 +510,4 @@ defineExpose({
     visibility: hidden;
   }
 }
-</style>
+</style>

+ 0 - 11
src/views/Tracking/src/components/TrackingTable/src/components/DownloadDialog.vue

@@ -139,17 +139,6 @@ defineExpose({
       font-size: 12px;
     }
 
-    .el-icon {
-      margin-left: 4px;
-      color: var(--color-theme);
-      transform: rotate(0deg);
-      transition: all 0.3s;
-
-      &.is-rotate {
-        transform: rotate(180deg);
-      }
-    }
-
     .select-columns {
       padding: 8px;
       margin-top: 8px;

+ 565 - 0
src/views/Tracking/src/components/TrackingTable/src/components/VGMView.vue

@@ -0,0 +1,565 @@
+<script setup lang="ts">
+import dayjs from 'dayjs'
+import { useRoute, useRouter } from 'vue-router'
+import { autoWidth } from '@/utils/table'
+import { type VxeGridInstance, type VxeGridProps } from 'vxe-table'
+
+const route = useRoute()
+const router = useRouter()
+
+const allData = ref<any>({})
+const loading = ref(false)
+const generalInfo = ref({
+  baseInfo: [
+    {
+      label: 'HBL No.',
+      value: ''
+    },
+    {
+      label: 'Carrier Booking No.',
+      value: ''
+    },
+    {
+      label: 'Vessel',
+      value: ''
+    },
+    {
+      label: 'Voyage',
+      value: ''
+    },
+    {
+      label: 'ETD',
+      value: ''
+    },
+    {
+      label: 'ETA ',
+      value: ''
+    },
+    {
+      label: 'Last Updated User',
+      value: ''
+    },
+    {
+      label: 'Last Updated Time',
+      value: ''
+    }
+  ],
+  formData: {
+    Submitter: '',
+    signature: '',
+    authorized_email: '',
+    authorized_tel: '',
+    is_send: false
+  }
+})
+const tableRef = ref<VxeGridInstance | null>(null)
+const tableData = ref<VxeGridProps<any>>({
+  minHeight: 70,
+  height: '330',
+  border: true,
+  round: true,
+  columns: [],
+  data: [
+    {
+      container_no: '123',
+      carrier_booking_no: '123',
+      size: '123',
+      vgm_weight: '123',
+      vgm_kg_lg: '12311111111111111111111111111',
+      vgm_time: '',
+      vgm_method: '123',
+      cargo_weight_kg: '123',
+      cargo_weight_lb: '123'
+    },
+    {
+      container_no: '123',
+      carrier_booking_no: '123',
+      size: '123',
+      vgm_weight: '123',
+      vgm_kg_lg: '123',
+      vgm_time: '',
+      vgm_method: '123',
+      cargo_weight_kg: '123',
+      cargo_weight_lb: '123'
+    }
+  ],
+  scrollY: { enabled: true, oSize: 20, gt: 30 },
+  stripe: true,
+  emptyText: ' ',
+  showHeaderOverflow: true,
+  showOverflow: true,
+  headerRowStyle: {
+    backgroundColor: 'var(--color-table-header-bg)'
+  },
+  editConfig: {
+    enabled: true,
+    trigger: 'dblclick', // 触发编辑的方式,可以是 click 或者 dbclick
+    mode: 'row',
+    showIcon: true
+  },
+  columnConfig: { resizable: true, useKey: true },
+  rowConfig: { isHover: true }
+})
+const handleColumns = (columns: any) => {
+  const newColumns = columns.map((item: any) => {
+    let curColumn: any = {
+      title: item.title,
+      field: item.field
+    }
+
+    // 添加编辑插槽
+    if (item.edit_type === 'input') {
+      curColumn = {
+        ...curColumn,
+        editRender: {
+          name: 'vInput'
+        },
+        slots: {
+          edit: 'vInput'
+        }
+      }
+    } else if (item.edit_type === 'dateTime') {
+      curColumn = {
+        ...curColumn,
+        editRender: {
+          name: 'editDate'
+        },
+        slots: {
+          edit: 'editDate'
+        }
+      }
+    }
+
+    if (item.title === 'VGM Unit') {
+      curColumn = {
+        ...curColumn,
+        editRender: {
+          name: 'vUnitSelect'
+        },
+        slots: {
+          edit: 'vUnitSelect'
+        }
+      }
+    } else if (item.title === 'VGM Method') {
+      curColumn = {
+        ...curColumn,
+        editRender: {
+          name: 'vMethodSelect'
+        },
+        slots: {
+          edit: 'vMethodSelect'
+        }
+      }
+    }
+    // 格式化
+    if (item.edit_type === 'dateTime') {
+      curColumn = {
+        ...curColumn,
+        formatter: ({ cellValue }: any) =>
+          cellValue ? dayjs(cellValue).format('MMM-YYYY-DD HH:mm:ss') : '--'
+      }
+    }
+    return curColumn
+  })
+  return newColumns
+}
+
+const convertData = (data: any) => {
+  const handleIsSend = (value: string) => {
+    if (value === 'f' || value === '') {
+      return false
+    } else if (value === 't') {
+      return true
+    }
+  }
+  generalInfo.value = {
+    baseInfo: [
+      {
+        label: 'HBL No.',
+        value: data?.['HBL No.']
+      },
+      {
+        label: 'Carrier Booking No.',
+        value: data?.['Carrier Booking No.']
+      },
+      {
+        label: 'Vessel',
+        value: data.Vessel
+      },
+      {
+        label: 'Voyage',
+        value: data.Voyage
+      },
+      {
+        label: 'ETD',
+        value: data.ETD
+      },
+      {
+        label: 'ETA ',
+        value: data.ETA
+      },
+      {
+        label: 'Last Updated User',
+        value: data?.['Last updated User']
+      },
+      {
+        label: 'Last Updated Time',
+        value: data?.['Last updated Time']
+      }
+    ],
+    formData: {
+      Submitter: data.Submitter,
+      signature: data.signature,
+      authorized_email: data.authorized_email,
+      authorized_tel: data.authorized_tel,
+      is_send: handleIsSend(data.is_send)
+    }
+  }
+}
+const getData = () => {
+  loading.value = true
+  $api
+    .getVGMData({
+      a: route.query.a,
+      _schemas: route.query._schemas
+    })
+    .then((res: any) => {
+      if (res.code === 200) {
+        allData.value = res.data
+        convertData(res.data?.general_information)
+        tableData.value.columns = handleColumns(
+          res.data?.detail_information?.detail_information_column
+        )
+        tableData.value.data = res.data?.detail_information?.detail_information_data
+        nextTick(() => {
+          tableRef.value && autoWidth(tableData.value, tableRef.value)
+          tableData.value.columns.forEach((item) => {
+            if (item.title === 'SN') {
+              item.width = 50
+            } else if (item.title === 'VGM Time') {
+              item.width = 210
+            }
+          })
+        })
+      }
+    })
+    .finally(() => {
+      loading.value = false
+    })
+}
+getData()
+
+const handleGoBack = () => {
+  router.push({ name: 'Tracking' })
+}
+const handleSave = () => {
+  const generalData = {
+    all_carrier_booking: generalInfo.value.baseInfo['Carrier Booking No.'],
+    submitter: generalInfo.value.formData.Submitter,
+    signature: generalInfo.value.formData.signature,
+    authorized_email: generalInfo.value.formData.authorized_email,
+    authorized_tel: generalInfo.value.formData.authorized_tel,
+    is_send: generalInfo.value.formData.is_send
+  }
+  const tableRowData = tableData.value.data.map((item) => {
+    return {
+      ...item
+    }
+  })
+
+  const variableList = Object.keys(tableRowData[0])
+  const tableInfo = {}
+  variableList.forEach((item) => {
+    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') : ''
+        )
+      })
+      return
+    }
+    Object.assign(tableInfo, {
+      [item]: tableRowData.map((row) => row[item])
+    })
+  })
+  console.log({ ...tableInfo, ...generalData })
+  $api
+    .saveVGMData({
+      serial_no: allData.value.serial_no,
+      _schemas: allData.value.schemas,
+      ...generalData,
+      ...tableInfo
+    })
+    .then((res) => {
+      console.log(res, '数据')
+      if (res.code === 200) {
+        // router.push({ name: 'Tracking' })
+      }
+    })
+}
+</script>
+
+<template>
+  <div class="vgm" v-vloading="loading">
+    <div class="header">
+      <div class="title">Add VGM</div>
+      <div class="right-option">
+        <el-button class="el-button--default" @click="handleGoBack"
+          ><span class="font_family icon-icon_return_b"></span> Cancel</el-button
+        >
+        <el-button class="el-button--main" @click="handleSave">
+          <span class="font_family icon-icon_save_b"></span>
+          Save</el-button
+        >
+      </div>
+    </div>
+    <div class="content">
+      <div class="general-info">
+        <div class="title">
+          <span>General Infomation</span>
+        </div>
+        <div class="description-info">
+          <div class="data-info" v-for="item in generalInfo.baseInfo" :key="item.label">
+            <div class="label">{{ item.label }}</div>
+            <div class="info">{{ item.value }}</div>
+          </div>
+        </div>
+        <div class="form">
+          <div class="form-row">
+            <div class="form-item">
+              <div class="label">Submitter <span class="require-asterisk">*</span></div>
+              <div class="content">
+                <el-input
+                  v-model="generalInfo.formData.Submitter"
+                  placeholder="Please enter..."
+                  clearable
+                ></el-input>
+              </div>
+            </div>
+            <div class="form-item">
+              <div class="label">Signature <span class="require-asterisk">*</span></div>
+              <div class="content">
+                <el-input
+                  v-model="generalInfo.formData.signature"
+                  placeholder="Please enter..."
+                  clearable
+                ></el-input>
+              </div>
+            </div>
+            <div style="width: 130px"></div>
+          </div>
+          <div class="form-row">
+            <div class="form-item">
+              <div class="label">Authorized Email <span class="require-asterisk">*</span></div>
+              <div class="content">
+                <el-input
+                  v-model="generalInfo.formData.authorized_email"
+                  placeholder="Please enter..."
+                  clearable
+                ></el-input>
+              </div>
+            </div>
+            <div class="form-item">
+              <div class="label">
+                Authorized Tel
+                <span class="require-asterisk">*</span>
+              </div>
+              <div class="content">
+                <el-input
+                  v-model="generalInfo.formData.authorized_tel"
+                  placeholder="Please enter..."
+                  clearable
+                ></el-input>
+              </div>
+            </div>
+            <div class="form-item" style="flex: 0 0 130px">
+              <div class="label"></div>
+              <div class="content">
+                <el-checkbox v-model="generalInfo.formData.is_send" label="Is Send" size="large" />
+              </div>
+            </div>
+          </div>
+        </div>
+      </div>
+      <div class="detail-info" style="margin-top: 8px">
+        <div class="title">
+          <span>Detail Information</span>
+        </div>
+        <div class="table">
+          <vxe-grid ref="tableRef" class="vgm-table" v-bind="tableData">
+            <template #vInput="{ row, column }">
+              <el-input
+                v-model="row[column.field]"
+                placeholder="Please enter..."
+                clearable
+              ></el-input>
+            </template>
+
+            <template #vUnitSelect="{ row, column }">
+              <vxe-select v-model="row[column.field]" placeholder="Please select..." clearable>
+                <vxe-option label="KGS" value="KGS"></vxe-option>
+                <vxe-option label="LBS" value="LBS"></vxe-option>
+              </vxe-select>
+            </template>
+
+            <template #vMethodSelect="{ row, column }">
+              <vxe-select v-model="row[column.field]" placeholder="Please select..." clearable>
+                <vxe-option label="SM1" value="SM1"></vxe-option>
+                <vxe-option label="SM2" value="SM2"></vxe-option>
+              </vxe-select>
+            </template>
+
+            <template #editDate="{ row, column }">
+              <el-date-picker
+                v-model="row[column.field]"
+                type="datetime"
+                style="width: 190px"
+                placeholder="Pick a Date"
+                format="MMM-DD-YYYY HH:mm:ss"
+                date-format="MMM-DD-YYYY"
+                time-format="HH:mm:ss"
+              />
+              <!-- <a-date-picker
+                :showNow="false"
+                class="test-date-picker"
+                placement="topLeft"
+                v-model:value="row[column.field]"
+                format="MMM-DD-YYYY HH:mm:ss"
+                :getPopupContainer="(target) => target.parentElement"
+                :show-time="{ defaultValue: dayjs('00:00:00', 'HH:mm:ss') }"
+              /> -->
+            </template>
+          </vxe-grid>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.vgm {
+  .header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    height: 72px;
+    padding: 0 24px;
+    border-bottom: 1px solid var(--color-border);
+    .title {
+      font-size: 24px;
+      font-weight: 700;
+    }
+    .right-option {
+      .el-button {
+        height: 40px;
+        padding: 8px 24px;
+        .font_family {
+          margin-right: 4px;
+        }
+      }
+    }
+  }
+  & > .content {
+    padding: 8px 24px 16px;
+  }
+  .general-info,
+  .detail-info {
+    border: 1px solid var(--color-border);
+    border-radius: 12px;
+    & > .title {
+      height: 48px;
+      line-height: 48px;
+      span {
+        font-size: 18px;
+        font-weight: 700;
+      }
+    }
+    & > .description-info {
+      display: flex;
+      flex-wrap: wrap;
+      padding: 8px 16px 0;
+    }
+    .form {
+      padding: 16px;
+      border-top: 1px solid var(--color-border);
+    }
+  }
+  .general-info {
+    & > .title {
+      padding-left: 16px;
+    }
+  }
+  .detail-info {
+    padding: 0 16px 8px;
+  }
+  .form {
+    .form-row {
+      display: flex;
+      flex-wrap: wrap;
+      gap: 16px;
+      margin-bottom: 16px;
+      .form-item {
+        flex: 1;
+        .label {
+          height: 16px;
+          margin-bottom: 8px;
+          font-size: 12px;
+          line-height: 16px;
+          color: var(--dashboard-text-color);
+          .require-asterisk {
+            margin-left: -3px;
+            font-size: 16px;
+            color: #c9353f;
+          }
+        }
+        .content {
+          display: flex;
+          align-items: center;
+          height: 32px;
+          .el-input {
+            width: 100%;
+          }
+        }
+      }
+    }
+  }
+}
+.data-info {
+  display: flex;
+  flex-direction: column;
+  width: 25%;
+  margin-bottom: 20px;
+  .label {
+    margin-bottom: 8px;
+    font-size: 12px;
+    line-height: 16px;
+    color: var(--dashboard-text-color);
+  }
+  .info {
+    height: 21px;
+    line-height: 21px;
+    font-weight: 700;
+  }
+}
+</style>
+<style lang="scss">
+.el-checkbox__input.is-checked + .el-checkbox__label {
+  color: var(--color-neutral-1);
+}
+// .test-date-picker {
+//   .ant-picker-dropdown {
+//     // top: 40px !important;
+//   }
+//   .anticon svg {
+//     color: #202020;
+//   }
+// }
+
+// .vgm-table {
+//   .vxe-grid .vxe-grid--table-wrapper,
+//   div.vxe-table--body-wrapper {
+//     overflow: visible;
+//   }
+// }
+</style>

+ 3 - 1
tsconfig.app.json

@@ -1,5 +1,4 @@
 {
-  "extends": "@vue/tsconfig/tsconfig.dom.json",
   "include": [
     "env.d.ts",
     "src/**/*",
@@ -12,6 +11,9 @@
     "src/**/__tests__/*"
   ],
   "compilerOptions": {
+    "outDir": "./dist/app",
+    "module": "nodenext",
+    "moduleResolution": "nodenext",
     "composite": true,
     "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
     "baseUrl": ".",

+ 9 - 3
tsconfig.json

@@ -1,10 +1,10 @@
 {
   "compilerOptions": {
     "target": "esnext",
-    "module": "esnext",
+    "module": "node16",
     "noImplicitAny": false,
     "useDefineForClassFields": true,
-    "moduleResolution": "node",
+    "moduleResolution": "nodenext",
     "strict": true,
     "jsx": "preserve",
     "sourceMap": true,
@@ -22,7 +22,13 @@
       "esnext",
       "dom",
       "ES6"
-    ]
+    ],
+    "baseUrl": ".",
+    "paths": {
+      "@/*": [
+        "src/*"
+      ]
+    }
   },
   "include": [
     "packages",

+ 6 - 5
tsconfig.node.json

@@ -11,9 +11,10 @@
     "composite": true,
     "noEmit": true,
     "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
-
-    "module": "ESNext",
-    "moduleResolution": "Bundler",
-    "types": ["node"]
+    "outDir": "./dist/node",
+    "module": "node16",
+    "types": [
+      "node"
+    ]
   }
-}
+}

+ 1 - 1
vite.config.ts

@@ -1,5 +1,4 @@
 import { fileURLToPath, URL } from 'node:url'
-
 import { defineConfig } from 'vite'
 import vue from '@vitejs/plugin-vue'
 import AutoImport from 'unplugin-auto-import/vite'
@@ -10,6 +9,7 @@ import IconsResolver from 'unplugin-icons/resolver'
 
 // https://vitejs.dev/config/
 export default defineConfig({
+  base: '/k_new_online/',
   resolve: {
     alias: {
       '@': fileURLToPath(new URL('./src', import.meta.url))