Browse Source

fea:merge dv

AmandaG 10 months ago
parent
commit
af7a27122f
26 changed files with 608 additions and 76 deletions
  1. 2 0
      src/stores/modules/user.ts
  2. 30 0
      src/stores/modules/visitedRow.ts
  3. 12 4
      src/styles/icons/iconfont.css
  4. 0 0
      src/styles/icons/iconfont.js
  5. 4 0
      src/styles/icons/iconfont.svg
  6. BIN
      src/styles/icons/iconfont.ttf
  7. BIN
      src/styles/icons/iconfont.woff
  8. BIN
      src/styles/icons/iconfont.woff2
  9. 20 21
      src/styles/theme-g.scss
  10. 20 5
      src/styles/theme.scss
  11. 51 2
      src/styles/vxeTable.scss
  12. 1 1
      src/views/Booking/src/components/BookingDetail/src/BookingDetail.vue
  13. 45 1
      src/views/Booking/src/components/BookingTable/src/BookingTable.vue
  14. 3 3
      src/views/Layout/src/components/Header/HeaderView.vue
  15. 9 2
      src/views/Tracking/src/components/PublicTracking/src/PublicTrackingSearch.vue
  16. 32 17
      src/views/Tracking/src/components/PublicTracking/src/components/BasicInformation.vue
  17. 38 2
      src/views/Tracking/src/components/PublicTracking/src/components/PublicTrackingDetail.vue
  18. 110 0
      src/views/Tracking/src/components/PublicTracking/src/components/ShareLinkDialog.vue
  19. BIN
      src/views/Tracking/src/components/PublicTracking/src/images/share-link.png
  20. 44 6
      src/views/Tracking/src/components/TrackingDetail/src/TrackingDetail.vue
  21. 9 8
      src/views/Tracking/src/components/TrackingDetail/src/components/BasicInformation.vue
  22. 3 1
      src/views/Tracking/src/components/TrackingDetail/src/components/MapView.vue
  23. 115 0
      src/views/Tracking/src/components/TrackingDetail/src/components/ShareLinkDialog.vue
  24. 15 2
      src/views/Tracking/src/components/TrackingDetail/src/components/TransportStep.vue
  25. BIN
      src/views/Tracking/src/components/TrackingDetail/src/images/share-link.png
  26. 45 1
      src/views/Tracking/src/components/TrackingTable/src/TrackingTable.vue

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

@@ -1,4 +1,5 @@
 import { defineStore } from 'pinia'
+import { useVisitedRowState } from './visitedRow'
 
 interface UserState {
   username: string
@@ -31,6 +32,7 @@ export const useUserStore = defineStore('user', {
         localStorage.removeItem('isFirstLogin')
       }
       this.isFirstLogin = false
+      useVisitedRowState().clearVisitedRow()
     }
   }
 })

+ 30 - 0
src/stores/modules/visitedRow.ts

@@ -0,0 +1,30 @@
+import { defineStore } from 'pinia'
+
+interface VisitedRowState {
+  trackingTableData: Array<String>
+  bookingTableData: Array<String>
+}
+
+export const useVisitedRowState = defineStore('visitedRowState', {
+  state: (): VisitedRowState => ({
+    trackingTableData: JSON.parse(localStorage.getItem('trackingTableData')) || [],
+    bookingTableData: JSON.parse(localStorage.getItem('bookingTableData')) || []
+  }),
+  getters: {},
+  actions: {
+    setTrackingTableData(item: String) {
+      this.trackingTableData.push(item)
+      localStorage.setItem('trackingTableData', JSON.stringify(this.trackingTableData))
+    },
+    setBookingTableData(item: String) {
+      this.bookingTableData.push(item)
+      localStorage.setItem('bookingTableData', JSON.stringify(this.bookingTableData))
+    },
+    clearVisitedRow() {
+      this.trackingTableData = []
+      this.bookingTableData = []
+      localStorage.removeItem('trackingTableData')
+      localStorage.removeItem('bookingTableData')
+    }
+  }
+})

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

@@ -1,9 +1,9 @@
 @font-face {
   font-family: "font_family"; /* Project id 4672385 */
-  src: url('iconfont.woff2?t=1733995908700') format('woff2'),
-       url('iconfont.woff?t=1733995908700') format('woff'),
-       url('iconfont.ttf?t=1733995908700') format('truetype'),
-       url('iconfont.svg?t=1733995908700#font_family') format('svg');
+  src: url('iconfont.woff2?t=1736324058852') format('woff2'),
+       url('iconfont.woff?t=1736324058852') format('woff'),
+       url('iconfont.ttf?t=1736324058852') format('truetype'),
+       url('iconfont.svg?t=1736324058852#font_family') format('svg');
 }
 
 .font_family {
@@ -14,6 +14,14 @@
   -moz-osx-font-smoothing: grayscale;
 }
 
+.icon-icon_currentlink_b:before {
+  content: "\e6fc";
+}
+
+.icon-icon_jumplink_b1:before {
+  content: "\e6fd";
+}
+
 .icon-icon_arrow_b:before {
   content: "\e6fb";
 }

File diff suppressed because it is too large
+ 0 - 0
src/styles/icons/iconfont.js


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

@@ -14,6 +14,10 @@
     />
       <missing-glyph />
       
+      <glyph glyph-name="icon_currentlink_b" unicode="&#59132;" d="M102.4 384a409.6 409.6 0 1 0 819.2 0A409.6 409.6 0 0 0 102.4 384zM512 870.4a486.4 486.4 0 1 1 0-972.8A486.4 486.4 0 0 1 512 870.4z m0-742.336a256 256 0 1 1 0 512 256 256 0 0 1 0-512z"  horiz-adv-x="1024" />
+      
+      <glyph glyph-name="icon_jumplink_b1" unicode="&#59133;" d="M636.416 633.28h-252.16v96h368a48 48 0 0 0 48-48v-368h-96v252.16l-370.56-370.624-67.904 67.84 370.56 370.624z m305.6-610.24h-832v96h832v-96z"  horiz-adv-x="1088" />
+      
       <glyph glyph-name="icon_arrow_b" unicode="&#59131;" d="M491.328 763.456a32 32 0 0 0 58.56 0L882.56 5.76a32 32 0 0 0-41.216-42.56l-308.8 123.52a32 32 0 0 1-23.744 0l-308.8-123.52a32 32 0 0 0-41.216 42.56L491.328 763.456z"  horiz-adv-x="1088" />
       
       <glyph glyph-name="icon_dark_b1" unicode="&#59129;" d="M7.616 738.432a38.4 38.4 0 0 0 38.4 38.4h622.08a38.4 38.4 0 0 0 38.4-38.4v-111.68h148.992a38.4 38.4 0 0 0 29.632-14.08l150.528-183.36a38.4 38.4 0 0 0 8.768-24.32v-289.088a38.4 38.4 0 0 0-38.4-38.4h-71.68a133.184 133.184 0 0 0-250.752 3.648 38.208 38.208 0 0 0-15.424-3.2H383.616a133.184 133.184 0 0 0-249.728 0H46.08a38.4 38.4 0 0 0-38.4 38.4v622.08z m698.88-529.792a133.184 133.184 0 0 0 232.832-54.4h28.288V391.296l-130.304 158.72H706.56v-341.312z m-76.8-53.888V700.032H84.48v-545.28h44.672a133.184 133.184 0 0 0 259.328 0h241.28zM358.72 443.776V566.784h-76.8v-161.408a38.4 38.4 0 0 1 38.4-38.4H526.08v76.8H358.656z m-99.904-263.04a56.32 56.32 0 1 1 0-112.768 56.32 56.32 0 0 1 0 112.704z m494.528-56.384a56.32 56.32 0 1 1 112.64 0 56.32 56.32 0 0 1-112.64 0z"  horiz-adv-x="1088" />

BIN
src/styles/icons/iconfont.ttf


BIN
src/styles/icons/iconfont.woff


BIN
src/styles/icons/iconfont.woff2


+ 20 - 21
src/styles/theme-g.scss

@@ -4,31 +4,31 @@
   // 横幅
   --dashboard-scoring-bg-color: linear-gradient(
     270deg,
-    rgba(255, 182, 121,0.1) 0.9%,
-    rgba(118, 145, 255,0.1) 49.92%,
-    rgba(96, 242, 255,0.1) 98.78%
+    rgba(255, 182, 121, 0.1) 0.9%,
+    rgba(118, 145, 255, 0.1) 49.92%,
+    rgba(96, 242, 255, 0.1) 98.78%
   );
-  --scoring-colurful-color: linear-gradient(251deg, rgba(113, 103, 99) 0%,rgba(92, 106, 125) 100%);
+  --scoring-colurful-color: linear-gradient(251deg, rgba(113, 103, 99) 0%, rgba(92, 106, 125) 100%);
 
-  // button 
+  // button
   --color-btn-default-bg-color: transparent;
-  --color-btn-default-bg-hover:rgba(255, 117, 0, 0.2);
-  --color-btn-default-dark-bg: #ED6D00;
+  --color-btn-default-bg-hover: rgba(255, 117, 0, 0.2);
+  --color-btn-default-dark-bg: #ed6d00;
   --color-btn-default-dark-hover: #fff;
-  --color-grey: #3C414A;
+  --color-grey: #3c414a;
 
   --management-bg-color: #3e454f;
-  --more-type-bg-color: #343A43;
+  --more-type-bg-color: #343a43;
 
   // filterstag
   --color-tag-checked-all: rgba(255, 117, 0, 0.2);
   --color-tag-all-bg: rgba(255, 117, 0, 0.2);
-  --color-tag-all-bg-color:rgba(226,99,148,0.2);
+  --color-tag-all-bg-color: rgba(226, 99, 148, 0.2);
   --color-tag-created-bg: rgba(1, 103, 251, 0.2);
   --color-tag-confirmed-bg: rgba(179, 232, 93, 0.15);
   --color-tag-cancelled-bg: rgba(240, 241, 243, 0.2);
-  --color-tag-cancelled: rgba(240,241,243,0.7)
-  --color-tag-departure: #40A6E5;
+  --color-tag-cancelled: rgba(240, 241, 243, 0.7);
+  --color-tag-departure: #40a6e5;
   --color-tag-departure-bg: rgba(64, 166, 229, 0.2);
   --color-tag-cargo-received-bg: rgba(111, 124, 241, 0.2);
   --color-tag-cargo-received: #6f7cf1;
@@ -36,24 +36,23 @@
   --color-tag-completed-bg: rgba(91, 180, 98, 0.2);
 
   --color-select-border: #656f7d;
-  --border-color-2: #4C515A;
+  --border-color-2: #4c515a;
   --border-hover-color: rgba(255, 117, 0, 0.2);
 
-
   --el-color-info: #fff;
   --el-border-color-light: #3e454f;
   --el-checkbox-bg-color: #656f7d;
 
   // 日历
   --color-orange-6: #614d3f;
-  --color-range-text: #ED6D00;
+  --color-range-text: #ed6d00;
 
   // more filters
-  --color-table-header-bg: #343A43;
+  --color-table-header-bg: #343a43;
   --icon-color-black: #fff;
-  --more-filters-background-color: #2A2E34;
-  --addparties-background-color: #343A43;
-  --color-border-top: #3F434A;
+  --more-filters-background-color: #2a2e34;
+  --addparties-background-color: #343a43;
+  --color-border-top: #3f434a;
 
   // tag
   --tag-bg-color: rgba(239, 239, 240, 0.1);
@@ -66,8 +65,8 @@
   --scoring-smile-radio-color: #626871;
 
   // 输入框禁用的颜色
-  --input-disabled-bg-color: rgba(244,244,244,0.2);
+  --input-disabled-bg-color: rgba(244, 244, 244, 0.2);
   --input-disabled-text-color: #66696f;
-  --color-recent-name: rgba(240,241,243,0.1);
+  --color-recent-name: rgba(240, 241, 243, 0.1);
   --color-disabled-bg: #2b2b2c;
 }

+ 20 - 5
src/styles/theme.scss

@@ -81,8 +81,6 @@
 
   --color-mune-active-bg: #fdf5f1;
 
-  --color-table-header-bg: #f8f9fd;
-
   --color-dialog-header-bg: #f6f8fa;
   --color-dialog-body-bg: #ffffff;
   --color-drawer-body-bg: #fff;
@@ -185,7 +183,7 @@
 
   --color-message-box-header-bg: #f6f8fa;
   --color-table-header-bg: #f8f9fd;
-  --color-table-click-row-bg: #ffe3cc;
+  --color-table-click-row-bg: #fff1e6;
 
   --color-download-file-filter-tag-bg: #eceef0;
   --color-download-file-selected-column-tag-bg: #eceef0;
@@ -222,6 +220,10 @@
 
   --color-container-status-node-bg: #f8f9fd;
 
+  --color-table-stripe-bg: #ffffff;
+  --color-table-row-hover-bg: rgba(255, 117, 0, 0.1);
+
+  --color-share-link-bg: #f8f9fd;
   // 输入框禁用的颜色
   --input-disabled-bg-color: #f5f7fa;
   --input-disabled-text-color: #a8abb2;
@@ -240,6 +242,10 @@
   .el-input {
     --el-border: #eaebed;
   }
+
+  --color-vxe-table-visited-row-bg: #f2f2f2;
+
+  --color-public-tracking-empty-bg: #fff;
 }
 
 :root.dark {
@@ -295,6 +301,13 @@
   --color-btn-default-dark-hover-bg: #d56200;
 
   --color-btn-icon-bg: #3f434a;
+
+  --color-table-stripe-bg: #2b2f36;
+  --color-table-row-hover-bg: rgba(255, 117, 0, 0.1);
+
+  --color-share-link-bg: #3a4149;
+
+  --color-public-tracking-empty-bg: #2b2f36;
   // 滚动条
   --color-scrollbar-thumb: #656f7d;
 
@@ -361,6 +374,8 @@
   --vxe-ui-table-column-icon-border-color: #6a6d73;
   --color-table-header-bg: #30353c;
   --vxe-ui-table-header-background-color: #30353c;
-  --color-table-click-row-bg: #8b582f;
+  --color-table-click-row-bg: #403631;
   --vxe-ui-input-border-color: #656f7d;
-}
+  --vxe-ui-table-menu-background-color: #3e454f;
+  --color-vxe-table-visited-row-bg: #3c4149;
+}

+ 51 - 2
src/styles/vxeTable.scss

@@ -18,7 +18,15 @@
 
 // 重置表格stripe样式
 .vxe-table--render-default tr.vxe-body--row.row--stripe {
-  background-color: var(--color-table-header-bg);
+  background-color: var(--color-table-stripe-bg);
+}
+.vxe-table--render-default tr.vxe-body--row.row--current,
+.vxe-table--render-default tr.vxe-body--row.row--stripe.row--current {
+  background-color: rgba(255, 117, 0, 0.2);
+}
+.vxe-table--render-default .vxe-body--row.visited-row,
+.vxe-table--render-default .vxe-body--row.row--stripe.visited-row {
+  background-color: var(--color-vxe-table-visited-row-bg);
 }
 
 .vxe-table--render-default .vxe-body--column.col--ellipsis,
@@ -64,8 +72,9 @@ div.vxe-table .vxe-sort--desc-btn.sort--active {
   background-color: var(--color-table-click-row-bg) !important;
 }
 div.vxe-table--render-default tr.vxe-body--row.row--hover,
+div.vxe-table--render-default tr.vxe-body--row.row--hover.row--current,
 .vxe-table--render-default tr.vxe-body--row.row--stripe.row--hover {
-  background-color: var(--border-hover-color);
+  background-color: var(--color-table-row-hover-bg);
 }
 div.vxe-table--render-default tr.vxe-body--row.vxe-table-row-clicked-style {
   background-color: var(--border-hover-color);
@@ -110,3 +119,43 @@ button.w-e-menu-tooltip-v5:before {
 button.w-e-menu-tooltip-v5:after {
   border: 0;
 }
+
+div.vxe-table--context-menu-wrapper {
+  padding: 8px;
+  border-radius: 12px;
+  box-shadow: 4px 4px 16px 0px rgba(0, 0, 0, 0.1);
+  border: none;
+  .vxe-context-menu--option-wrapper {
+    border: none;
+  }
+  li {
+    display: flex;
+    align-items: center;
+    height: 40px;
+    border: none;
+    border-radius: 6px;
+    .vxe-context-menu--link {
+      width: auto;
+      .vxe-context-menu--link-suffix {
+        display: none;
+      }
+      .vxe-context-menu--link-prefix {
+        i {
+          color: var(--color-neutral-1);
+        }
+      }
+    }
+    &.link--active {
+      background-color: var(--border-hover-color);
+
+      .vxe-context-menu--link-content {
+        color: var(--color-theme);
+      }
+      .vxe-context-menu--link-prefix {
+        i {
+          color: var(--color-theme);
+        }
+      }
+    }
+  }
+}

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

@@ -212,7 +212,7 @@ const { isOverflow: isDestinationOverflow } = useOverflow(destinationRef, allDat
           </div>
 
           <!-- Containers -->
-          <div v-if="item.id === 2">
+          <div v-if="item.id === 2 && allData?.transportInfo?.mode !== 'Air Freight'">
             <VBox :id="item.id" :isSeeAll="false" @draggable="handleDraggable">
               <template #header>Containers</template>
               <template #content>

+ 45 - 1
src/views/Booking/src/components/BookingTable/src/BookingTable.vue

@@ -8,7 +8,9 @@ import { ref, onMounted } from 'vue'
 import { useRouter } from 'vue-router'
 import { transportationMode } from '@/components/TransportationMode'
 import { useThemeStore } from '@/stores/modules/theme'
+import { useVisitedRowState } from '@/stores/modules/visitedRow'
 
+const visitedRowState = useVisitedRowState()
 const themeStore = useThemeStore()
 const router = useRouter()
 const props = defineProps({
@@ -194,6 +196,19 @@ const bookingTable = ref<VxeGridProps<any>>({
   headerRowStyle: {
     backgroundColor: 'var(--color-table-header-bg)'
   },
+  menuConfig: {
+    body: {
+      options: [
+        [
+          {
+            code: 'newTab',
+            name: 'Open in New Tab',
+            prefixConfig: { icon: 'icon-icon_jumplink_b1 font_family' }
+          }
+        ]
+      ]
+    }
+  },
   sortConfig: {
     sortMethod: (params) => {
       const { data, sortList } = params
@@ -241,7 +256,7 @@ const bookingTable = ref<VxeGridProps<any>>({
     }
   },
   columnConfig: { resizable: true, useKey: true },
-  rowConfig: { isHover: true },
+  rowConfig: { isHover: true, isCurrent: true },
   exportConfig: {
     types: ['csv', 'html', 'txt', 'xlsx'],
     modes: ['current', 'selected', 'all']
@@ -388,6 +403,7 @@ const handleCellDblclick = ({ row }: any) => {
     path: '/booking/detail',
     query: { a: row.__serial_no, _schemas: row._schemas, status: row.Status }
   })
+  visitedRowState.setBookingTableData(row['__serial_no'])
 }
 // 点击link字段是时
 const handleLinkClick = (row: any, column: any) => {
@@ -396,6 +412,7 @@ const handleLinkClick = (row: any, column: any) => {
       path: '/booking/detail',
       query: { a: row.__serial_no, _schemas: row._schemas, status: row.Status }
     })
+    visitedRowState.setBookingTableData(row['__serial_no'])
   } else if (column.title === 'HBL No.') {
     router.push({
       path: '/tracking/detail',
@@ -415,6 +432,30 @@ const handleCheckAllChange = ({ records }: any) => {
   selectedNumber.value = records.length
   selectedTableData.value = records
 }
+
+// 表格右键点击事件
+const handleCurrentRow = (params) => {
+  tableRef.value?.setCurrentRow(params.row)
+}
+// 表格右键菜单点击事件
+const handleTableMenuClick = ({ menu }) => {
+  if (menu.code === 'newTab') {
+    const curRow = tableRef.value?.getCurrentRecord()
+    window.open(
+      `${window.location.protocol}//${window.location.host}/tracking/detail?a=${curRow.__serial_no}&_schemas=${curRow._schemas}`,
+      '_blank'
+    )
+    visitedRowState.setBookingTableData(curRow['__serial_no'])
+  }
+}
+
+// 修改已查看详情行的样式
+const handleRowClassName = ({ row }: any) => {
+  if (visitedRowState.bookingTableData.includes(row['__serial_no'])) {
+    return 'visited-row'
+  }
+  return ''
+}
 defineExpose({
   searchTableData,
   getLoadingData,
@@ -451,10 +492,13 @@ defineExpose({
       v-vloading="tableLoadingTable || tableLoadingColumn"
       :height="props.height"
       :style="{ border: 'none' }"
+      :row-class-name="handleRowClassName"
       v-bind="bookingTable"
       @cell-dblclick="handleCellDblclick"
       @checkbox-change="handleCheckboxChange"
       @checkbox-all="handleCheckAllChange"
+      @menu-click="handleTableMenuClick"
+      @cell-menu="handleCurrentRow"
     >
       <!-- 空数据时的插槽 -->
       <template #empty v-if="!tableLoadingTable && bookingTable.data.length === 0">

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

@@ -360,10 +360,10 @@ div.el-popover.el-popper.toggle-theme-popover {
   width: 400px;
   height: 278px;
   padding: 16px;
-  // right: 20px !important;
-  // top: 52px !important;
-  // left: auto !important;
   inset: 56px 52.8px auto auto !important;
+  .el-popper__arrow {
+    display: none;
+  }
   .header {
     display: flex;
     justify-content: space-between;

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

@@ -93,7 +93,11 @@ const encryptPassword = (password) => {
 </script>
 
 <template>
-  <div class="public-tracking-search" v-vloading="loading" :class="{ 'dark-bg': themeStore.theme }">
+  <div
+    class="public-tracking-search"
+    v-vloading="loading"
+    :class="{ 'dark-bg': themeStore.theme === 'dark' }"
+  >
     <div class="search-info">
       <div class="title">Tracking</div>
       <el-input
@@ -206,7 +210,7 @@ const encryptPassword = (password) => {
     width: 480px;
     height: 315px;
     padding: 16px;
-    background-color: white;
+    background-color: var(--color-public-tracking-empty-bg);
     border-radius: 12px;
     border: 1px solid var(--color-border);
     .suggestion-info {
@@ -218,6 +222,9 @@ const encryptPassword = (password) => {
   div.multiple-empty {
     height: 236px;
     padding-top: 24px;
+    :deep(.suggestion) {
+      display: none;
+    }
   }
 }
 .public-tracking-search-input {

+ 32 - 17
src/views/Tracking/src/components/PublicTracking/src/components/BasicInformation.vue

@@ -9,64 +9,71 @@ const allData: any = ref({
     top: [
       {
         label: 'MAWB/MBL No.',
-        content: 'B83131200164'
+        content: ''
       },
       {
         label: 'HAWB/HBL No.',
-        content: 'ADSZXA600324',
+        content: '',
         type: 'link'
       },
       {
         label: 'Booking No.',
-        content: 'NAM1766636'
+        content: ''
       },
       {
         label: 'PO No.',
-        content: '258904-3'
+        content: ''
+      },
+      {
+        label: 'Vessel / Airline',
+        content: ''
+      },
+      {
+        label: 'Voyage / Flight',
+        content: ''
       }
     ]
   },
   businessPartners: [
     {
       title: 'Origin Agent',
-      company: 'Dynarex Corp.',
-      address: '1975 LINDEN BLVD 3RD FL SUITE # 300 ELMONT',
-      phone: '+1 212 5551234'
+      company: '',
+      address: '',
+      phone: ''
     },
     {
       title: 'Destination Agent',
-      company: 'Dynarex Corp.',
-      address: '1975 LINDEN BLVD 3RD FL SUITE # 300 ELMONT',
-      phone: '+1 212 5551234'
+      company: '',
+      address: '',
+      phone: ''
     }
   ],
   packing: [
     {
       label: 'Quantity / Unit',
-      content: 'ADSZXA600324'
+      content: ''
     },
     {
       label: 'G. Weight',
-      content: 'ADSZXA600324'
+      content: ''
     },
     {
       label: 'Ch. Weight',
-      content: 'ADSZXA600324'
+      content: ''
     },
     {
       label: 'Volume',
-      content: 'ADSZXA600324'
+      content: ''
     }
   ],
   marksAndDescription: [
     {
       label: 'Marks',
-      content: 'TO: ABC COMPANY, NEW YORK, USA / PACKAGES 1/50 / FRAGILE'
+      content: ''
     },
     {
       label: 'Description',
-      content:
-        '50 CARTONS OF CERAMIC VASES, GROSS WEIGHT 1000KG, NET WEIGHT 950KG, MADE IN CHINAERAMIC VASES, GROSS WEIGHT 1000KG,50 CARTONS OF CERAMIC VASES, GROSS WEIGHT 1000KG, NET WEIGHT 950KG, MADE IN CHINA 50 CARTONS OF CERAMIC VASES, GROSS WEIGHT 1000KG, NET WEIGHT 950KG, MADE IN CHINAERAMIC VASES, GROSS WEIGHT 1000KG,50 CARTONS OF CERAMIC VASES, GROSS WEIGHT 1000KG, NET WEIGHT 950KG, MADE IN CHINA 50 CARTONS OF CERAMIC VASES, GROSS WEIGHT 1000KG, NET WEIGHT 950KG, MADE IN CHINAERAMIC VASES, GROSS WEIGHT 1000KG,50 CARTONS OF CERAMIC VASES, GROSS WEIGHT 1000KG, NET WEIGHT 950KG, MADE IN CHINA 50 CARTONS OF CERAMIC VASES, GROSS WEIGHT 1000KG, NET WEIGHT 950KG, MADE IN CHINAERAMIC VASES, GROSS WEIGHT 1000KG,50 CARTONS OF CERAMIC VASES, GROSS WEIGHT 1000KG, NET WEIGHT 950KG, MADE '
+      content: ''
     }
   ]
 })
@@ -89,6 +96,14 @@ const convertData = (data: any) => {
         {
           label: 'PO No.',
           content: data.basicInfo.PO_NO || '--'
+        },
+        {
+          label: 'Vessel / Airline',
+          content: data.basicInfo['Vessel/Airline'] || '--'
+        },
+        {
+          label: 'Voyage / Flight',
+          content: data.basicInfo['Voyage/Filght'] || '--'
         }
       ]
     },

+ 38 - 2
src/views/Tracking/src/components/PublicTracking/src/components/PublicTrackingDetail.vue

@@ -6,6 +6,7 @@ import { useRoute } from 'vue-router'
 import { useOverflow } from '@/hooks/useOverflow'
 import { useThemeStore } from '@/stores/modules/theme'
 import { formatTimezone } from '@/utils/tools'
+import ShareLinkDialog from './ShareLinkDialog.vue'
 
 const route = useRoute()
 
@@ -71,10 +72,21 @@ const originRef = ref()
 const destinationRef = ref()
 const { isOverflow: isOriginOverflow } = useOverflow(originRef, allData)
 const { isOverflow: isDestinationOverflow } = useOverflow(destinationRef, allData)
+
+const dialogVModel = ref(false)
+const openShareDialog = () => {
+  dialogVModel.value = true
+}
 </script>
 
 <template>
   <div class="tracking-detail">
+    <!-- 分享链接 -->
+    <div class="share-link" @click="openShareDialog">
+      <el-tooltip content="Share" :offset="4">
+        <span class="font_family icon-icon_share_b share-icon"></span>
+      </el-tooltip>
+    </div>
     <div class="header" :class="{ 'is-dark': themeStore.theme === 'dark' }">
       <div class="detail-status">
         <span
@@ -82,8 +94,10 @@ const { isOverflow: isDestinationOverflow } = useOverflow(destinationRef, allDat
           :class="[`icon-${transportationMode?.[allData?.transportInfo?.mode]}`]"
           style="font-size: 64px"
         ></span>
-        <div class="no">Tracking No. {{ allData.transportInfo['Tracking No.'] }}</div>
-        <VTag large :type="allData.transportInfo?.status">{{ allData.transportInfo?.status }}</VTag>
+        <div class="no">Tracking No. {{ allData?.transportInfo['Tracking No.'] }}</div>
+        <VTag large :type="allData?.transportInfo?.status">{{
+          allData?.transportInfo?.status
+        }}</VTag>
       </div>
       <div class="detail-info">
         <div class="item transport-way">
@@ -172,6 +186,7 @@ const { isOverflow: isDestinationOverflow } = useOverflow(destinationRef, allDat
         </template>
       </VBox>
     </div>
+    <ShareLinkDialog v-model="dialogVModel"></ShareLinkDialog>
   </div>
 </template>
 
@@ -184,6 +199,7 @@ const { isOverflow: isDestinationOverflow } = useOverflow(destinationRef, allDat
   border-right: 1px solid var(--color-border);
 }
 .tracking-detail {
+  position: relative;
   padding-bottom: 16px;
   & > .header {
     background: linear-gradient(
@@ -294,6 +310,26 @@ const { isOverflow: isDestinationOverflow } = useOverflow(destinationRef, allDat
       }
     }
   }
+  .share-link {
+    position: fixed;
+    bottom: 152px;
+    right: 0px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    width: 64px;
+    height: 64px;
+    z-index: 1500;
+    background-color: var(--management-bg-color);
+    box-shadow: -2px 2px 12px rgba(0, 0, 0, 0.15);
+    border-radius: 12px 0 0 12px;
+    &:hover {
+      background-color: var(--border-hover-color);
+    }
+    .share-icon {
+      font-size: 24px;
+    }
+  }
 }
 
 .info-content {

+ 110 - 0
src/views/Tracking/src/components/PublicTracking/src/components/ShareLinkDialog.vue

@@ -0,0 +1,110 @@
+<script setup lang="ts">
+const dialogVModel = defineModel('dialogVModel', {
+  type: Boolean,
+  default: false
+})
+
+const shareLink = computed(() => {
+  return `${window.location.href}`
+})
+
+// 是否已复制的标志
+const copied = ref(false)
+
+// 复制到剪贴板的函数
+const copyToClipboard = async () => {
+  try {
+    // 使用 navigator.clipboard.writeText() 复制文本
+    await navigator.clipboard.writeText(shareLink.value)
+    ElMessage.success('Copied to clipboard')
+    // 设置已复制标志
+    copied.value = true
+
+    // 一段时间后重置标志
+    setTimeout(() => {
+      copied.value = false
+    }, 2000)
+  } catch (err) {
+    ElMessage.error('Failed to copy')
+  }
+}
+</script>
+
+<template>
+  <el-dialog width="480" top="25vh" class="share-link-dialog" v-model="dialogVModel">
+    <div class="share-link-body">
+      <div class="title">
+        <img src="../images/share-link.png" />
+      </div>
+      <div class="tips">
+        <span>Copy the link to share this shipment with anyone.</span>
+      </div>
+      <div class="link">
+        <span>{{ shareLink }}</span>
+      </div>
+    </div>
+    <template #footer>
+      <el-button
+        style="width: 240px; height: 40px"
+        class="el-button--dark"
+        @click="copyToClipboard"
+      >
+        <span class="font_family icon-icon_text_links_b"></span>
+        <span style="margin-left: 4px">Copy Link</span>
+      </el-button>
+    </template>
+  </el-dialog>
+</template>
+
+<style lang="scss" scoped>
+.share-link-body {
+  padding: 0 20px 8px;
+  .title {
+    text-align: center;
+    img {
+      width: 100px;
+    }
+  }
+  .tips {
+    margin-top: 10px;
+    text-align: center;
+    span {
+      color: var(--color-neutral-1);
+      font-size: 18px;
+      font-weight: 700;
+    }
+  }
+  .link {
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    height: 80px;
+    margin-top: 8px;
+    padding: 0 10px;
+    border-radius: 12px;
+    background-color: var(--color-share-link-bg);
+    span {
+      text-align: center;
+      color: var(--color-theme);
+      text-decoration: none;
+    }
+  }
+}
+</style>
+<style lang="scss">
+.share-link-dialog {
+  button.el-dialog__headerbtn {
+    height: 64px;
+    width: 64px;
+  }
+  .el-dialog__header {
+    background-color: var(--color-dialog-body-bg);
+  }
+  .el-dialog__body {
+    padding-top: 0;
+  }
+  .el-dialog__footer {
+    text-align: center;
+  }
+}
+</style>

BIN
src/views/Tracking/src/components/PublicTracking/src/images/share-link.png


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

@@ -15,6 +15,7 @@ import { useRoute } from 'vue-router'
 import { useOverflow } from '@/hooks/useOverflow'
 import { formatTimezone } from '@/utils/tools'
 import { useThemeStore } from '@/stores/modules/theme'
+import ShareLinkDialog from './components/ShareLinkDialog.vue'
 
 const route = useRoute()
 
@@ -78,9 +79,6 @@ const handleDraggable = (type: string, id: number) => {
 }
 
 const emailDrawerRef = ref()
-const handleEmailDrawer = () => {
-  emailDrawerRef.value.openDrawer(allData.value)
-}
 
 const AMSISFDrawerRef = ref()
 const handleAMSISF = () => {
@@ -112,10 +110,21 @@ const originRef = ref()
 const destinationRef = ref()
 const { isOverflow: isOriginOverflow } = useOverflow(originRef, allData)
 const { isOverflow: isDestinationOverflow } = useOverflow(destinationRef, allData)
+
+const dialogVModel = ref(false)
+const openShareDialog = () => {
+  dialogVModel.value = true
+}
 </script>
 
 <template>
   <div class="tracking-detail" v-vloading="loading">
+    <!-- 分享链接 -->
+    <div class="share-link" @click="openShareDialog">
+      <el-tooltip content="Share" :offset="4">
+        <span class="font_family icon-icon_share_b share-icon"></span>
+      </el-tooltip>
+    </div>
     <div class="header" :class="{ 'is-dark': themeStore.theme === 'dark' }">
       <div class="detail-status">
         <span
@@ -204,7 +213,12 @@ const { isOverflow: isDestinationOverflow } = useOverflow(destinationRef, allDat
       </div>
     </div>
     <div class="transport-map">
-      <MapView style="flex: 1" :serial_no="allData?.serial_no" :uncode="allData?.uncode"></MapView>
+      <MapView
+        style="flex: 1"
+        :_schemas="allData?._schemas"
+        :serial_no="allData?.serial_no"
+        :uncode="allData?.uncode"
+      ></MapView>
       <TransportStep class="transport-step" :data="allData"></TransportStep>
     </div>
     <div class="info-content">
@@ -245,7 +259,7 @@ const { isOverflow: isDestinationOverflow } = useOverflow(destinationRef, allDat
           </div>
 
           <!-- Containers -->
-          <div v-if="item.id === 2">
+          <div v-if="item.id === 2 && allData?.transportInfo?.mode !== 'Air Freight'">
             <VBox :id="item.id" :isSeeAll="false" @draggable="handleDraggable">
               <template #header>Containers</template>
               <template #content>
@@ -287,8 +301,11 @@ const { isOverflow: isDestinationOverflow } = useOverflow(destinationRef, allDat
       </VueDraggable>
       <EmailDrawer @sendEmailSuccess="getData" :data="allData" ref="emailDrawerRef"></EmailDrawer>
     </div>
-
     <AMSISFDrawer ref="AMSISFDrawerRef"></AMSISFDrawer>
+    <ShareLinkDialog
+      v-model="dialogVModel"
+      :searchNo="allData?.basicInfo?.['HAWB/HBOL']"
+    ></ShareLinkDialog>
   </div>
 </template>
 
@@ -301,6 +318,7 @@ const { isOverflow: isDestinationOverflow } = useOverflow(destinationRef, allDat
   border-right: 1px solid var(--color-border);
 }
 .tracking-detail {
+  position: relative;
   padding-bottom: 16px;
   & > .header {
     background: linear-gradient(
@@ -430,6 +448,26 @@ const { isOverflow: isDestinationOverflow } = useOverflow(destinationRef, allDat
     margin-bottom: 8px;
     padding: 0 24px;
   }
+  .share-link {
+    position: fixed;
+    bottom: 152px;
+    right: 0px;
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    width: 64px;
+    height: 64px;
+    z-index: 1500;
+    background-color: var(--management-bg-color);
+    box-shadow: -2px 2px 12px rgba(0, 0, 0, 0.15);
+    border-radius: 12px 0 0 12px;
+    &:hover {
+      background-color: var(--border-hover-color);
+    }
+    .share-icon {
+      font-size: 24px;
+    }
+  }
 }
 
 .ghost-class {

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

@@ -1,10 +1,7 @@
 <script setup lang="ts">
-import { useRouter } from 'vue-router'
 import XEClipboard from 'xe-clipboard'
 import AddReferenceDialog from './AddReferenceDialog.vue'
 
-const router = useRouter()
-
 const props = defineProps({
   data: Object
 })
@@ -104,6 +101,10 @@ const allData: any = ref({
     {
       label: 'Description',
       content: ''
+    },
+    {
+      label: 'Remark',
+      content: ''
     }
   ]
 })
@@ -204,14 +205,14 @@ const convertData = (data: any) => {
       {
         label: 'Description',
         content: data.marksAndDescription.description || '--'
+      },
+      {
+        label: 'Remark',
+        content: data.marksAndDescription.remark || '--'
       }
     ]
   }
 }
-// 跳转到shipment页面
-const handLink = (id: string) => {
-  router.push({ path: '/tracking', query: { id } })
-}
 
 // 复制文本
 const handleCopy = (data: any) => {
@@ -423,7 +424,7 @@ defineExpose({
         </div>
       </div>
     </div>
-    <AddReferenceDialog ref="addReferenceRef"></AddReferenceDialog>
+    <!-- <AddReferenceDialog ref="addReferenceRef"></AddReferenceDialog> -->
   </div>
 </template>
 

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

@@ -20,6 +20,7 @@ const themeStore = useThemeStore()
 const props = defineProps<{
   serial_no?: string
   uncode?: string
+  _schemas?: string
 }>()
 
 const markerPositions = ref([])
@@ -147,7 +148,8 @@ const getMarker = () => {
   $api
     .getTrackingDetailMapData({
       serial_no: props.serial_no,
-      uncode: props.uncode
+      uncode: props.uncode,
+      _schemas: props._schemas
     })
     .then((res) => {
       if (res.code === 200) {

+ 115 - 0
src/views/Tracking/src/components/TrackingDetail/src/components/ShareLinkDialog.vue

@@ -0,0 +1,115 @@
+<script setup lang="ts">
+const dialogVModel = defineModel('dialogVModel', {
+  type: Boolean,
+  default: false
+})
+const props = defineProps({
+  searchNo: {
+    type: String,
+    default: ''
+  }
+})
+const shareLink = computed(() => {
+  return `${window.location.protocol}//${window.location.host}/public-tracking/detail?searchNo=${props.searchNo || ''}`
+})
+
+// 是否已复制的标志
+const copied = ref(false)
+
+// 复制到剪贴板的函数
+const copyToClipboard = async () => {
+  try {
+    // 使用 navigator.clipboard.writeText() 复制文本
+    await navigator.clipboard.writeText(shareLink.value)
+    ElMessage.success('Copied to clipboard')
+    // 设置已复制标志
+    copied.value = true
+
+    // 一段时间后重置标志
+    setTimeout(() => {
+      copied.value = false
+    }, 2000)
+  } catch (err) {
+    ElMessage.error('Failed to copy')
+  }
+}
+</script>
+
+<template>
+  <el-dialog width="480" top="25vh" class="share-link-dialog" v-model="dialogVModel">
+    <div class="share-link-body">
+      <div class="title">
+        <img src="../images/share-link.png" />
+      </div>
+      <div class="tips">
+        <span>Copy the link to share this shipment with anyone.</span>
+      </div>
+      <div class="link">
+        <span>{{ shareLink }}</span>
+      </div>
+    </div>
+    <template #footer>
+      <el-button
+        style="width: 240px; height: 40px"
+        class="el-button--dark"
+        @click="copyToClipboard"
+      >
+        <span class="font_family icon-icon_text_links_b"></span>
+        <span style="margin-left: 4px">Copy Link</span>
+      </el-button>
+    </template>
+  </el-dialog>
+</template>
+
+<style lang="scss" scoped>
+.share-link-body {
+  padding: 0 20px 8px;
+  .title {
+    text-align: center;
+    img {
+      width: 100px;
+    }
+  }
+  .tips {
+    margin-top: 10px;
+    text-align: center;
+    span {
+      color: var(--color-neutral-1);
+      font-size: 18px;
+      font-weight: 700;
+    }
+  }
+  .link {
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    height: 80px;
+    margin-top: 8px;
+    padding: 0 10px;
+    border-radius: 12px;
+    background-color: var(--color-share-link-bg);
+    span {
+      text-align: center;
+      color: var(--color-theme);
+      text-decoration: none;
+    }
+  }
+}
+</style>
+<style lang="scss">
+.share-link-dialog {
+  button.el-dialog__headerbtn {
+    height: 64px;
+    width: 64px;
+  }
+  .el-dialog__header {
+    background-color: var(--color-dialog-body-bg);
+  }
+  .el-dialog__body {
+    padding-top: 0;
+  }
+  .el-dialog__footer {
+    text-align: center;
+  }
+}
+</style>

+ 15 - 2
src/views/Tracking/src/components/TrackingDetail/src/components/TransportStep.vue

@@ -15,24 +15,34 @@ const handleTabClick = (name: string) => {
     <div class="header">
       <div
         class="tab"
-        :class="{ 'is-active': activeName === 'shipmentStatus' }"
+        :class="{
+          'is-active':
+            activeName === 'shipmentStatus' && props.data?.transportInfo?.mode !== 'Air Freight',
+          'only-one-mode': props.data?.transportInfo?.mode === 'Air Freight'
+        }"
         @click="handleTabClick('shipmentStatus')"
       >
         Shipment Status
       </div>
       <div
+        v-if="props.data?.transportInfo?.mode !== 'Air Freight'"
         class="tab"
         :class="{ 'is-active': activeName === 'containerStatus' }"
         @click="handleTabClick('containerStatus')"
       >
         Container Status
       </div>
+      <div v-else class="tab"></div>
     </div>
     <div class="content">
       <template v-if="activeName === 'shipmentStatus'">
         <ShipmentStatus :data="props.data?.simplexData" />
       </template>
-      <template v-else-if="activeName === 'containerStatus'">
+      <template
+        v-else-if="
+          activeName === 'containerStatus' && props.data?.transportInfo?.mode !== 'Air Freight'
+        "
+      >
         <ContainerStatus :data="props.data?.containerStatusData" />
       </template>
     </div>
@@ -64,6 +74,9 @@ const handleTabClick = (name: string) => {
         border-bottom: 2px solid var(--color-theme);
         color: var(--color-neutral-1);
       }
+      &.only-one-mode {
+        color: var(--color-neutral-1);
+      }
     }
   }
   .content {

BIN
src/views/Tracking/src/components/TrackingDetail/src/images/share-link.png


+ 45 - 1
src/views/Tracking/src/components/TrackingTable/src/TrackingTable.vue

@@ -8,7 +8,9 @@ import { ref, onMounted } from 'vue'
 import { transportationMode } from '@/components/TransportationMode'
 import { useLoadingState } from '@/stores/modules/loadingState'
 import { useThemeStore } from '@/stores/modules/theme'
+import { useVisitedRowState } from '@/stores/modules/visitedRow'
 
+const visitedRowState = useVisitedRowState()
 const themeStore = useThemeStore()
 
 const router = useRouter()
@@ -268,6 +270,19 @@ const trackingTable = ref<any>({
   headerRowStyle: {
     backgroundColor: 'var(--color-table-header-bg)'
   },
+  menuConfig: {
+    body: {
+      options: [
+        [
+          {
+            code: 'newTab',
+            name: 'Open in New Tab',
+            prefixConfig: { icon: 'icon-icon_jumplink_b1 font_family' }
+          }
+        ]
+      ]
+    }
+  },
   sortConfig: {
     sortMethod: (params) => {
       const { data, sortList } = params
@@ -315,7 +330,7 @@ const trackingTable = ref<any>({
     }
   },
   columnConfig: { resizable: true, useKey: true },
-  rowConfig: { isHover: true },
+  rowConfig: { isHover: true, isCurrent: true },
   exportConfig: {
     types: ['csv', 'html', 'txt', 'xlsx'],
     modes: ['current', 'selected', 'all']
@@ -467,6 +482,7 @@ const handleCellDblclick = ({ row }: any) => {
     path: '/tracking/detail',
     query: { a: row.__serial_no, _schemas: row._schemas }
   })
+  visitedRowState.setTrackingTableData(row['__serial_no'])
 }
 // 点击link字段时
 const handleLinkClick = (row: any, column: any) => {
@@ -480,6 +496,7 @@ const handleLinkClick = (row: any, column: any) => {
       path: '/tracking/detail',
       query: { a: row.__serial_no, _schemas: row._schemas }
     })
+    visitedRowState.setTrackingTableData(row['__serial_no'])
   }
 }
 
@@ -505,6 +522,30 @@ const handleVGM = (row) => {
 
 const loadingState = useLoadingState()
 
+// 表格右键点击事件
+const handleCurrentRow = (params) => {
+  tableRef.value?.setCurrentRow(params.row)
+}
+// 表格右键菜单点击事件
+const handleTableMenuClick = ({ menu }) => {
+  if (menu.code === 'newTab') {
+    const curRow = tableRef.value?.getCurrentRecord()
+    visitedRowState.setTrackingTableData(curRow['__serial_no'])
+    window.open(
+      `${window.location.protocol}//${window.location.host}/tracking/detail?a=${curRow.__serial_no}&_schemas=${curRow._schemas}`,
+      '_blank'
+    )
+  }
+}
+
+// 修改已查看详情行的样式
+const handleRowClassName = ({ row }: any) => {
+  if (visitedRowState.trackingTableData.includes(row['__serial_no'])) {
+    return 'visited-row'
+  }
+  return ''
+}
+
 defineExpose({
   searchTableData,
   getSharedTableData,
@@ -543,10 +584,13 @@ defineExpose({
       v-vloading="tableLoadingColumn || tableLoadingTable || loadingState.trackingTableLoading"
       :height="props.height"
       :style="{ border: 'none' }"
+      :row-class-name="handleRowClassName"
       v-bind="trackingTable"
       @cell-dblclick="handleCellDblclick"
       @checkbox-change="handleCheckboxChange"
       @checkbox-all="handleCheckAllChange"
+      @menu-click="handleTableMenuClick"
+      @cell-menu="handleCurrentRow"
     >
       <!-- 空数据时的插槽 -->
       <template #empty v-if="!tableLoadingTable && trackingTable.data.length === 0">

Some files were not shown because too many files changed in this diff