Jelajahi Sumber

Merge branch 'feat_theme_zyh' of United_Software/k_online_ui into feat_theme

Jack Zhou 11 bulan lalu
induk
melakukan
132171811d

+ 34 - 15
src/styles/elementui.scss

@@ -20,12 +20,14 @@ button.el-button.el-button--text {
   padding: 4px 8px;
   border: none;
   background-color: transparent;
-  span {
-    color: var(--color-theme);
-  }
+
   &:hover {
     background-color: var(--color-btn-default-bg-hover);
-    color: var(--color-theme);
+  }
+  &:active {
+    span {
+      color: var(--color-theme);
+    }
   }
 }
 
@@ -45,7 +47,7 @@ button.el-button.el-button--text {
   }
 }
 
-.el-button.el-button--main.is-plain {
+.el-button--main.is-plain {
   background-color: var(--color-white);
   border: 1px solid var(--color-border);
   span {
@@ -60,7 +62,7 @@ button.el-button.el-button--text {
   }
 }
 
-.el-button.el-button--main {
+button.el-button--main {
   border: none;
   background-color: var(--color-theme);
   span {
@@ -74,6 +76,23 @@ button.el-button.el-button--text {
   }
 }
 
+button.el-button.el-button--pain-theme {
+  border: 1px solid var(--color-el-btn-pain-theme-border);
+  background-color: var(--color-el-btn-pain-theme-bg);
+
+  fill: var(--color-el-btn-pain-theme-text);
+  color: var(--color-el-btn-pain-theme-text);
+  span {
+    color: var(--color-el-btn-pain-theme-text);
+  }
+  &:hover {
+    border-color: var(--color-el-btn-pain-theme-border);
+    background-color: var(--color-el-btn-pain-theme-bg-hover);
+    color: var(--color-white);
+    fill: var(--color-white);
+  }
+}
+
 .el-button.el-button--success {
   border: none;
   background-color: var(--color-success);
@@ -133,9 +152,9 @@ button.el-button.el-button--text {
 .el-button.el-button--blue {
   border: none;
   padding: 0 4px;
-  background-color: #f6f6fe;
-  color: var(--color-accent-1);
-  fill: var(--color-accent-1);
+  background-color: var(--color-btn-blue-bg);
+  color: var(--color-neutral-1);
+  fill: var(--color-neutral-1);
   &:hover {
     background-color: var(--color-btn-default-bg-hover);
     fill: var(--color-theme);
@@ -150,9 +169,9 @@ button.el-button.el-button--icon {
   height: auto;
   padding: 4px;
   border: none;
-  background-color: #f6f6fe;
+  background-color: var(--color-btn-icon-bg);
   &:hover {
-    background-color: #f6f6fe;
+    background-color: var(--color-btn-icon-bg);
   }
 }
 // 初始为黑色
@@ -165,8 +184,8 @@ button.el-button.el-button--icon {
   }
   fill: var(--color-white);
   &:hover {
-    background-color: var(--color-btn-default-dark-bg);
-    fill: var(--color-btn-default-dark-hover);
+    background-color: var(--color-btn-default-dark-hover-bg);
+    fill: var(--color-btn-default-dark-hover-bg);
     span {
       color: var(--color-btn-default-dark-hover) !important;
     }
@@ -388,7 +407,7 @@ div .el-checkbox__inner:hover {
 }
 div .el-checkbox__inner {
   border-color: var(--color-select-border);
-  background-color: #FFF;
+  background-color: #fff;
 }
 div .el-checkbox__input.is-checked .el-checkbox__inner:after {
   border-color: var(--color-mode);
@@ -748,4 +767,4 @@ div .DaterangeClass {
   background-color: var(--management-bg-color) !important;
   border-color: var(--management-bg-color) !important;
   border-radius: 12px !important;
-}
+}

+ 20 - 0
src/styles/theme.scss

@@ -200,6 +200,7 @@
   --color-customize-column-tabs-header-border: #ebeef5;
 
   --color-shipment-status-label-bg: #ccd1db;
+  --color-shipment-status-shadow: rgba(0, 0, 0, 0.1);
 
   --color-slider-bg: #fff;
   --color-slider-thumb-start: #2b2f36;
@@ -219,6 +220,12 @@
   --input-disabled-text-color: #a8abb2;
 
   --el-disabled-bg-color: #f4f4f4;
+
+  --color-btn-icon-bg: #f6f6fe;
+
+  --color-btn-blue-bg: #f6f6fe;
+
+  --color-btn-default-dark-hover-bg: #2b2f36;
 }
 
 :root.dark {
@@ -245,6 +252,7 @@
   --color-customize-column-item-drag-bg: #7d4c26;
   --color-customize-column-tabs-header-border: #3f434a;
 
+  --color-shipment-status-shadow: rgba(0, 0, 0, 0.5);
   --color-shipment-status-label-bg: #656f7d;
   --color-shipment-status-detail-path-font-color: #b5b7ba;
   --color-shipment-status-label-font-color: #c6cad0;
@@ -260,6 +268,17 @@
   --color-toggle-btn-module-active-bg: #656f7d;
 
   --color-user-config-title-bottom-border: #3f434a;
+
+  --color-btn-blue-bg: rgba(255, 255, 255, 0);
+
+  --color-el-btn-pain-theme-border: #ed6d00;
+  --color-el-btn-pain-theme-text: #ed6d00;
+  --color-el-btn-pain-theme-bg: rgba(237, 109, 0, 0.1);
+  --color-el-btn-pain-theme-bg-hover: rgba(237, 109, 0, 0.3);
+
+  --color-btn-default-dark-hover-bg: #d56200;
+
+  --color-btn-icon-bg: #3f434a;
   // 滚动条
   --color-scrollbar-thumb: #656f7d;
 
@@ -271,6 +290,7 @@
 
   --color-v-box-content-drag-bg: #2b2f36;
 
+  --color-input-disabled-border: #656f7d;
   // 邮件
   --w-e-toolbar-bg-color: var(--color-email-bg);
   --w-e-textarea-bg-color: var(--color-email-bg);

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

@@ -8,9 +8,12 @@ import { transportationMode } from '@/components/TransportationMode'
 import { useRoute } from 'vue-router'
 import { useOverflow } from '@/hooks/useOverflow'
 import { formatTimezone } from '@/utils/tools'
+import { useThemeStore } from '@/stores/modules/theme'
 
 const route = useRoute()
 
+const themeStore = useThemeStore()
+
 // 可拖拽模块的列表
 const boxList = ref([
   { id: 1, name: 'Basic Information' },
@@ -94,7 +97,7 @@ const { isOverflow: isDestinationOverflow } = useOverflow(destinationRef, allDat
 
 <template>
   <div class="booking-detail" v-vloading="loading">
-    <div class="header">
+    <div class="header" :class="{ 'is-dark': themeStore.theme === 'dark' }">
       <div class="detail-status">
         <span
           class="font_family"
@@ -243,6 +246,16 @@ const { isOverflow: isDestinationOverflow } = useOverflow(destinationRef, allDat
       rgba(224, 247, 249, 0.6) 80.46%,
       rgba(255, 255, 255, 0.3)
     );
+    &.is-dark {
+      background: linear-gradient(
+        270deg,
+        rgba(43, 47, 54, 0.1) 1.88%,
+        rgba(255, 182, 121, 0.1) 15.6%,
+        rgba(118, 145, 255, 0.1) 49.92%,
+        rgba(96, 242, 255, 0.1) 81.78%,
+        rgba(43, 47, 54, 0.1) 97.95%
+      );
+    }
 
     .detail-status {
       position: relative;

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

@@ -204,7 +204,7 @@ const sendEmail = () => {
         @click="handleFocusEditor"
       />
     </div>
-    <div style="border-bottom: 1px solid var(--color-border)">
+    <div style="border-bottom: 1px solid var(--color-divider)">
       <el-button
         @click="sendEmail"
         class="el-button--dark"
@@ -268,12 +268,19 @@ const sendEmail = () => {
   }
 
   & > .content {
+    display: inline-block;
+    flex: 1;
     padding-top: 2px;
     line-height: 18px;
     color: var(--color-theme);
     word-break: break-all;
   }
 }
+.separated-by {
+  :deep(.el-input__wrapper) {
+    box-shadow: 0 0 0 1px var(--color-email-border) inset;
+  }
+}
 
 .text-editor {
   margin-top: 16px;

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

@@ -7,7 +7,9 @@ import dayjs from 'dayjs'
 import { ref, onMounted } from 'vue'
 import { useRouter } from 'vue-router'
 import { transportationMode } from '@/components/TransportationMode'
+import { useThemeStore } from '@/stores/modules/theme'
 
+const themeStore = useThemeStore()
 const router = useRouter()
 const props = defineProps({
   height: {
@@ -430,7 +432,11 @@ defineExpose({
     <div class="table-tools">
       <div class="left-total-records">{{ selectedNumber }} Selected</div>
       <div class="right-tools-btn">
-        <el-button class="el-button--main" @click="handleDownload">
+        <el-button
+          :class="{ 'el-button--pain-theme': themeStore.theme === 'dark' }"
+          class="el-button--main"
+          @click="handleDownload"
+        >
           <span style="margin-right: 8px" class="font_family icon-icon_download_b"></span>
           Download
         </el-button>

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

@@ -174,7 +174,7 @@ const handleLogin = () => {
           <div class="header">
             <span class="title">Themes</span>
             <el-button @click="themePopoverRef.hide()" class="close-icon el-button--text">
-              <span class="font_family icon-icon_reject_b"></span>
+              <div class="font_family icon-icon_reject_b"></div>
             </el-button>
           </div>
           <div class="tips">
@@ -213,7 +213,7 @@ const handleLogin = () => {
           </div>
         </div>
         <template #reference>
-          <el-button>
+          <el-button style="height: 40px; width: 40px" class="el-button--text">
             <span class="font_family icon-icon_themes_b" style="font-size: 16px"></span
           ></el-button>
         </template>
@@ -252,9 +252,10 @@ const handleLogin = () => {
         </template>
       </el-popover>
       <el-button
+        :class="{ 'el-button--pain-theme': themeStore.theme === 'dark' }"
         v-if="!userStore.username || (userStore.username && userStore.isFirstLogin === true)"
         class="el-button--main"
-        style="padding: 8px 10px"
+        style="padding: 8px 10px; margin-right: 10px; margin-left: 0"
         plain
         @click="handleDownload"
       >
@@ -301,7 +302,7 @@ const handleLogin = () => {
   display: flex;
   align-items: center;
   justify-content: center;
-  gap: 24px;
+  gap: 8px;
   height: 100%;
 
   .el-input {

+ 7 - 3
src/views/Login/src/components/ChangePasswordCard.vue

@@ -149,7 +149,7 @@ onUnmounted(() => {
 <template>
   <div class="login" ref="changePasswordRef">
     <el-card class="login-card">
-      <div class="title">
+      <div class="title" :class="{ 'is-dark': themeStore.theme === 'dark' }">
         <span class="welcome">Change Password</span>
         <span class="tips">{{ tips }}</span>
       </div>
@@ -272,16 +272,19 @@ onUnmounted(() => {
 
 .login-card {
   width: 400px;
-
+  border-radius: 12px;
   .title {
     display: flex;
     flex-direction: column;
     align-items: center;
     margin-bottom: 24px;
     padding: 40px 40px 0;
-    background: url(../image/bg-login-card.png) no-repeat center center;
+    background: url(../image/bg-login-card.png) no-repeat;
     background-position: top left; /* 从左上角开始显示 */
     background-size: 400px 100px; /* 保持背景图像的原始尺寸 */
+    &.is-dark {
+      background: url(../image/bg-login-card-dark.png) no-repeat;
+    }
     .welcome {
       margin-bottom: 16px;
       font-size: 24px;
@@ -388,6 +391,7 @@ onUnmounted(() => {
   .el-input.user-name {
     :deep(.el-input__wrapper) {
       padding-right: 6px;
+      box-shadow: 0 0 0 1px var(--color-input-disabled-border) inset;
     }
   }
 

TEMPAT SAMPAH
src/views/Login/src/image/bg-dark.png


TEMPAT SAMPAH
src/views/Login/src/image/bg-login-card-dark.png


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

@@ -346,7 +346,7 @@ const firstLoginTipsRef = ref()
   <div class="login" ref="loginRef">
     <ScoringSystem class="scoring-system"></ScoringSystem>
     <el-card class="login-card" v-if="status === 'login'">
-      <div class="card-title">
+      <div class="card-title" :class="{ 'is-dark': themeStore.theme === 'dark' }">
         <span class="welcome">Welcome to KLN Portal</span>
         <span class="tips">Login to your account</span>
       </div>
@@ -415,7 +415,7 @@ const firstLoginTipsRef = ref()
       </template>
     </el-card>
     <el-card class="login-card" v-else-if="status === 'reset'">
-      <div class="card-title">
+      <div class="card-title" :class="{ 'is-dark': themeStore.theme === 'dark' }">
         <span class="welcome">Password Retrieval</span>
         <span class="tips">We'll send your password to your email address.</span>
       </div>
@@ -510,10 +510,12 @@ const firstLoginTipsRef = ref()
     align-items: center;
     height: 94px;
     padding-top: 32px;
-    background: url(../src/image/bg-login-card.png) no-repeat center center;
+    background: url(../src/image/bg-login-card.png) no-repeat;
     background-position: top left; /* 从左上角开始显示 */
     background-size: 400px 100px; /* 保持背景图像的原始尺寸 */
-
+    &.is-dark {
+      background: url(../src/image/bg-login-card-dark.png) no-repeat;
+    }
     .welcome {
       margin-bottom: 16px;
       font-size: 24px;

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

@@ -4,10 +4,13 @@ import MilestonesTable from './MilestonesTable.vue'
 import { transportationMode } from '@/components/TransportationMode'
 import { useRoute } from 'vue-router'
 import { useOverflow } from '@/hooks/useOverflow'
+import { useThemeStore } from '@/stores/modules/theme'
 import { formatTimezone } from '@/utils/tools'
 
 const route = useRoute()
 
+const themeStore = useThemeStore()
+
 const allData: any = ref({
   transportInfo: {
     'Tracking No.': '',
@@ -72,7 +75,7 @@ const { isOverflow: isDestinationOverflow } = useOverflow(destinationRef, allDat
 
 <template>
   <div class="tracking-detail">
-    <div class="header">
+    <div class="header" :class="{ 'is-dark': themeStore.theme === 'dark' }">
       <div class="detail-status">
         <span
           class="font_family"
@@ -191,6 +194,16 @@ const { isOverflow: isDestinationOverflow } = useOverflow(destinationRef, allDat
       rgba(224, 247, 249, 0.6) 80.46%,
       rgba(255, 255, 255, 0.3)
     );
+    &.is-dark {
+      background: linear-gradient(
+        270deg,
+        rgba(43, 47, 54, 0.1) 1.88%,
+        rgba(255, 182, 121, 0.1) 15.6%,
+        rgba(118, 145, 255, 0.1) 49.92%,
+        rgba(96, 242, 255, 0.1) 81.78%,
+        rgba(43, 47, 54, 0.1) 97.95%
+      );
+    }
 
     .detail-status {
       position: relative;

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

@@ -432,6 +432,7 @@ const { isOverflow: isDestinationOverflow } = useOverflow(destinationRef, allDat
       top: 8px;
       right: 16px;
       z-index: 1000;
+      overflow: hidden;
     }
   }
 }

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

@@ -59,9 +59,6 @@ watch(
         }
       ]
       tableData.value.data = attachment.document_data
-      // nextTick(() => {
-      //   tableRef.value && autoWidth(tableData.value, tableRef.value)
-      // })
     }
   },
   {

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

@@ -279,6 +279,11 @@ const sendEmail = () => {
     color: var(--color-theme);
   }
 }
+.separated-by {
+  :deep(.el-input__wrapper) {
+    box-shadow: 0 0 0 1px var(--color-email-border) inset;
+  }
+}
 
 .text-editor {
   margin-top: 16px;

+ 409 - 106
src/views/Tracking/src/components/TrackingDetail/src/components/MapView.vue

@@ -1,5 +1,11 @@
 <template>
-  <div id="tracking-map" style="width: 100%; height: 520px" class="tracking-map"></div>
+  <div
+    id="tracking-map"
+    ref="mapContainer"
+    style="width: 100%; height: 520px"
+    class="tracking-map"
+    :class="{ 'dark-mode': themeStore.theme === 'dark' }"
+  ></div>
 </template>
 <script setup lang="ts">
 import L from 'leaflet'
@@ -8,6 +14,10 @@ import OriginIcon from '../images/originIcon.png'
 import TransferIcon from '../images/transferIcon.png'
 import { onMounted, ref, watch } from 'vue'
 import * as turf from '@turf/turf'
+import { mapData } from './mapData'
+import { useThemeStore } from '@/stores/modules/theme'
+
+const themeStore = useThemeStore()
 
 const props = defineProps<{
   serial_no?: string
@@ -45,7 +55,7 @@ const initMap = () => {
     return
   }
 
-  map = L.map('tracking-map').setView([51.505, -0.09], 3)
+  map = L.map('tracking-map', {}).setView([51.505, -0.09], 3)
 
   // 添加 TileLayer
   L.tileLayer('https://map.kerryapex.com/osm_tiles/{z}/{x}/{y}.png', {
@@ -54,8 +64,6 @@ const initMap = () => {
   }).addTo(map)
 }
 
-const track_base_line = ref([])
-const track_first_pt = null
 // 修正经度
 const fixLng = (lng: number) => {
   while (lng > 180) lng = lng - 360
@@ -121,74 +129,84 @@ const getBBox = () => {
   if (bbox3.length > 0) ll.push(bbox3)
   return ll
 }
-const draw_marker = () => {
-  track_base_line.value.forEach((l) => {
-    addMapLine(l, true, false, { color: '#ff7500', weight: 2 })
+let track_added_marker = []
+
+const clear_marker = () => {
+  track_added_marker.forEach((v) => {
+    map!.removeLayer(v)
   })
+
+  track_added_marker = []
 }
-const addMapLine = (l, IsDash, HasArrow, opts) => {
+const draw_marker = (dottedLine = [], solidLine = []) => {
+  clear_marker()
+  dottedLine.forEach((l) => {
+    addMapLine(l, true, { color: '#ff7500', weight: 2 })
+  })
+  solidLine.forEach((l) => {
+    addMapLine(l, false, { color: '#ff7500', weight: 2 })
+  })
+}
+const addMapLine = (l, IsDash, opts) => {
   let mpts = l.pts
   if (mpts == null || mpts.length == 0) return
 
   let bnd = map.getBounds()
   let ww = bnd.getEast() - bnd.getWest()
   let cc = Math.ceil(ww / 360)
-  // let cent_pt = map.getCenter()
-  // let count = Math.ceil(cent_pt.lng / 360)
-
-  // let boxlist = getBBox()
-
-  // let ll = []
-
-  // for (let ii = 0; ii < mpts.length; ii++) {
-  //   ll[ii] = [mpts[ii][1], mpts[ii][0]]
-  // }
-  // let pline = turf.lineString(ll, { name: '' })
-  // let level = map.getZoom()
-  // let options = {
-  //   tolerance:
-  //     Math.round(
-  //       ((level < 8 ? 0.0005 : level < 12 ? 0.00005 : 0) / (level == 0 ? 1 : level)) * 1000000
-  //     ) / 1000000,
-  //   highQuality: false
-  // }
-  // let simplified = turf.simplify(pline, options)
-
-  // let lines_list = []
-  // let clipped: any = simplified
-  // boxlist.forEach((bbox) => {
-  //   let bb = bbox
-  //   clipped = turf.bboxClip(simplified, bb)
-  //   if (clipped != null) {
-  //     if (clipped.geometry.type === 'LineString') {
-  //       let line_pts = clipped.geometry.coordinates
-  //       let pta = []
-  //       let jj = 0
-  //       for (let ii = 0; ii < line_pts.length; ii++) {
-  //         pta[jj++] = [line_pts[ii][1], line_pts[ii][0]]
-  //       }
-  //       lines_list.push(pta)
-  //     } else if (clipped.geometry.type === 'MultiLineString') {
-  //       clipped.geometry.coordinates.forEach((_pts) => {
-  //         let line_pts = _pts
-  //         let pta = []
-  //         let jj = 0
-  //         for (let ii = 0; ii < line_pts.length; ii++) {
-  //           pta[jj++] = [line_pts[ii][1], line_pts[ii][0]]
-  //         }
-  //         lines_list.push(pta)
-  //       })
-  //     }
-  //   }
-  // })
+
+  let boxlist = getBBox()
+
+  let ll = []
+
+  for (let ii = 0; ii < mpts.length; ii++) {
+    ll[ii] = [mpts[ii][1], mpts[ii][0]]
+  }
+  let pline = turf.lineString(ll, { name: '' })
+  let level = map.getZoom()
+  let options = {
+    tolerance:
+      Math.round(
+        ((level < 8 ? 0.0005 : level < 12 ? 0.00005 : 0) / (level == 0 ? 1 : level)) * 1000000
+      ) / 1000000,
+    highQuality: false
+  }
+  let simplified = turf.simplify(pline, options)
+
+  let lines_list = []
+  let clipped: any = simplified
+  boxlist.forEach((bbox) => {
+    let bb = bbox
+    clipped = turf.bboxClip(simplified, bb)
+    if (clipped != null) {
+      if (clipped.geometry.type === 'LineString') {
+        let line_pts = clipped.geometry.coordinates
+        let pta = []
+        let jj = 0
+        for (let ii = 0; ii < line_pts.length; ii++) {
+          pta[jj++] = [line_pts[ii][1], line_pts[ii][0]]
+        }
+        lines_list.push(pta)
+      } else if (clipped.geometry.type === 'MultiLineString') {
+        clipped.geometry.coordinates.forEach((_pts) => {
+          let line_pts = _pts
+          let pta = []
+          let jj = 0
+          for (let ii = 0; ii < line_pts.length; ii++) {
+            pta[jj++] = [line_pts[ii][1], line_pts[ii][0]]
+          }
+          lines_list.push(pta)
+        })
+      }
+    }
+  })
 
   let cc1 = Math.floor(bnd.getWest() / 360)
   let cc2 = Math.ceil(bnd.getEast() / 360)
   cc = cc2 - cc1 + 1
 
   let kk = cc1
-  // let llIndex = 0
-  let lines_list = [mpts]
+  // let lines_list = [mpts]
   while (kk >= cc1 && kk <= cc2 && cc > 0) {
     lines_list.forEach((a) => {
       let ii = 0
@@ -197,16 +215,15 @@ const addMapLine = (l, IsDash, HasArrow, opts) => {
       for (ii = 0; ii < a.length; ii++) pts[jj++] = [a[ii][0], a[ii][1] + kk * 360]
 
       if (jj > 0) {
-        if (IsDash)
+        if (IsDash) {
           showTrackLine(
             pts,
             jj,
-            Object.assign({}, { color: '#ff7500', dashArray: '10', weight: 2 }, opts),
-            HasArrow
+            Object.assign({}, { color: '#ff7500', dashArray: '10', weight: 2 }, opts)
           )
-        else
-          showTrackLine(pts, jj, Object.assign({}, { color: '#ff7500', weight: 1 }, opts), HasArrow)
-        // llIndex++
+        } else {
+          showTrackLine(pts, jj, Object.assign({}, { color: '#ff7500', weight: 1 }, opts))
+        }
       }
     })
 
@@ -215,34 +232,9 @@ const addMapLine = (l, IsDash, HasArrow, opts) => {
   }
 }
 
-const showTrackLine = (pts, jj, opts, HasArrow) => {
+const showTrackLine = (pts, jj, opts) => {
   let arrow = L.polyline(pts, Object.assign({}, { color: '#ff7500', weight: 2 }, opts)).addTo(map)
-
-  // if (HasArrow) {
-  //   let arrowHead = L.polylineDecorator(arrow, {
-  //     patterns: [
-  //       {
-  //         offset: 0,
-  //         repeat: 25,
-  //         symbol: L.Symbol.arrowHead({
-  //           pixelSize: 10,
-  //           pathOptions: Object.assign({}, { color: '#ff7500', fillOpacity: 0.5, weight: 0.8 }, opts)
-  //         })
-  //       }
-  //     ]
-  //   }).addTo(map)
-  // }
-}
-
-const test = () => {
-  track_base_line.value = mapData
-  draw_marker()
-  // map.on('moveend', function (e) {
-  //   draw_marker()
-  // })
-  // map.on('zoomend', function (e) {
-  //   draw_marker()
-  // })
+  track_added_marker.push(arrow)
 }
 
 const addResetZoomButton = (center: L.LatLng, zoom: number) => {
@@ -280,10 +272,14 @@ let initialCenter: L.LatLng | null = null
 let initialZoomLevel: number | null = null
 let isFirstRender = true // 标记是否为首次渲染
 
+let allMarkers = []
+let visibleMarkers = new Set()
+
 // 添加标记后更新中心和缩放级别
 const addMarkersToMap = () => {
+  // debugger
   if (!map) return // 确保地图已经初始化
-  const latLngBounds: any = [] // 用来存储所有标记的坐标
+
   markerPositions.value.forEach((position) => {
     const marker = L.marker([position.lat, position.lng], { icon: position.icon }).addTo(map)
 
@@ -303,11 +299,14 @@ const addMarkersToMap = () => {
         closeOnClick: false
       })
       .openPopup()
-    latLngBounds.push([position.lat, position.lng])
+    allMarkers[`${position.lat},${position.lng}`] = marker
   })
 
-  if (latLngBounds.length > 0) {
-    const bounds = L.latLngBounds(latLngBounds)
+  updateVisibleMarkers()
+
+  if (viewData.value.length > 0) {
+    // 根据标记的位置设置中心点以及缩放级别
+    const bounds = L.latLngBounds(viewData.value)
     map!.fitBounds(bounds, { paddingTopLeft: [20, 70], paddingBottomRight: [400, 0] })
     setTimeout(() => {
       if (isFirstRender) {
@@ -320,6 +319,153 @@ const addMarkersToMap = () => {
   }
 }
 
+// 新增轮船当前位置标记
+const addShipMarker = (x: number) => {
+  const solidLine = allMapData.value.solidLine
+  // 如果轮船还未出发,则显示起点轮船标记
+  if (solidLine.length === 0) {
+    // 创建轮船图标
+    const arrowIcon = L.divIcon({
+      html: `
+        <div class="container">
+          <div class="circle"></div>
+          <span style="padding: 0; color:white; border:1px solid white" class="font_family icon-icon_ocean_b"></span>
+        </div>
+        `,
+      className: 'arrow-icon',
+      iconSize: [50, 50],
+      iconAnchor: [25, 25], // 箭头的中心点
+      popupAnchor: [0, -25] // 弹出框的锚点
+    })
+
+    let curMarkerLocation = markerPositions.value.find((item) => item.label === 'Origin')
+    const arrowMarker = L.marker([curMarkerLocation.lat, curMarkerLocation.lng + x * 360], {
+      icon: arrowIcon
+    }).addTo(map)
+    track_added_marker.push(arrowMarker)
+  } else if (solidLine.length > 0) {
+    // 如果轮船已经出发,则显示轮船当前位置标记
+    // 如果线段至少有两个点,才添加箭头
+    // 获取线段的最后一个点和倒数第二个点
+    const lastPoint = solidLine[solidLine.length - 1]
+    const secondLastPoint = solidLine[solidLine.length - 2]
+    console.log(lastPoint, secondLastPoint, 'lastPoint, secondLastPoint')
+    // 计算线段末端的角度(以弧度为单位)
+    const angle =
+      (Math.atan2(
+        Number(lastPoint.lon) - Number(secondLastPoint.lon), // Δlon (x)
+        Number(lastPoint.lat) - Number(secondLastPoint.lat) // Δlat (y)
+      ) *
+        (180 / Math.PI) +
+        360) %
+      360
+    // 创建自定义箭头图标
+    const arrowIcon = L.divIcon({
+      html: `
+      <div style="transform: rotate(${angle}deg);" class="container">
+        <div class="circle"></div>
+        <span style="color:white;border:1px solid white" class="font_family icon-icon_arrow_b"></span>
+      </div>
+      `,
+      className: 'arrow-icon',
+      iconSize: [50, 50],
+      iconAnchor: [25, 25], // 箭头的中心点
+      popupAnchor: [0, -25] // 弹出框的锚点
+    })
+    // 创建箭头标记,并根据计算出的角度旋转箭头
+    const arrowMarker = L.marker([Number(lastPoint.lat), Number(lastPoint.lon) + x * 360], {
+      icon: arrowIcon
+    }).addTo(map)
+    // 将箭头标记也存储在 track_added_marker 数组中,以便后续管理
+    track_added_marker.push(arrowMarker)
+  }
+}
+
+// 更新可见标记
+const updateVisibleMarkers = () => {
+  const newVisibleMarkers = new Set()
+
+  let bnd = map.getBounds()
+  let ww = bnd.getEast() - bnd.getWest()
+
+  let cc = Math.ceil(ww / 360)
+
+  let cc1 = Math.floor(bnd.getWest() / 360)
+  let cc2 = Math.ceil(bnd.getEast() / 360)
+  cc = cc2 - cc1 + 1
+
+  let x = cc1
+
+  // 移除不再可见的标记
+  visibleMarkers.forEach((marker: any) => {
+    if (!newVisibleMarkers.has(marker)) {
+      map.removeLayer(marker)
+      delete allMarkers[`${marker.getLatLng().lat},${marker._lng},${marker._x}`]
+    }
+  })
+
+  // 计算当前视图中的标记,包括多地球的情况
+  while (x >= cc1 && x <= cc2 && cc > 0) {
+    Object.values(allMarkers).forEach((marker) => {
+      const latLng = marker.getLatLng()
+      const key = `${latLng.lat},${latLng.lng},${x}`
+      if (!allMarkers[key]) {
+        const newMarker: any = L.marker([latLng.lat, latLng.lng + x * 360], {
+          icon: marker.options.icon
+        })
+          .bindPopup(marker.getPopup().getContent(), marker.getPopup().options)
+          .openPopup()
+        newMarker._x = x
+        // 使用原始的经度作为标记的唯一标识
+        newMarker._lng = latLng.lng
+        allMarkers[key] = newMarker
+        map.addLayer(newMarker)
+      }
+      newVisibleMarkers.add(allMarkers[key])
+    })
+
+    addShipMarker(x)
+
+    x++
+    cc--
+  }
+
+  // 更新可见标记集合
+  visibleMarkers = newVisibleMarkers
+}
+
+// 处理得到的数据
+const handleData = (data) => {
+  let key = 0
+  let curLine = []
+  let resultLine = []
+  data.forEach((item, index) => {
+    if (item.sn === '1' && key === 0) {
+      key++
+      curLine.push([Number(item.lat), Number(item.lon)])
+    } else if (item.sn === '1' && key !== 0) {
+      resultLine.push({
+        name: key,
+        pts: curLine
+      })
+      curLine = [[Number(item.lat), Number(item.lon)]]
+      key++
+    }
+    if (item.sn !== '1') {
+      curLine.push([Number(item.lat), Number(item.lon)])
+    }
+    if (index === data.length - 1 && item.sn !== '1') {
+      resultLine.push({
+        name: key,
+        pts: curLine
+      })
+    }
+  })
+  return resultLine
+}
+
+const allMapData = ref()
+const viewData = ref([])
 // 请求接口并处理标记
 const getMarker = () => {
   $api
@@ -329,9 +475,10 @@ const getMarker = () => {
     })
     .then((res) => {
       if (res.code === 200) {
+        allMapData.value = res.data
         const { data } = res
-        data &&
-          data.forEach((item) => {
+        data?.point &&
+          data?.point.forEach((item) => {
             const iconColorList = {
               Destination: { color: '#24ca5a', icon: destinationIcon },
               Origin: { color: '#ED6D00', icon: originIcon },
@@ -339,15 +486,31 @@ const getMarker = () => {
             }
             markerPositions.value.push({
               lat: item.lat,
-              lng: item.lng,
+              lng: (Number(item.lng) + 360) % 360,
               city: item.infor,
               label: item.label,
               icon: iconColorList[item.label].icon,
               iconColor: iconColorList[item.label].color
             })
           })
+        viewData.value = (data?.rangePoint.length > 0 ? data?.rangePoint : data?.point)?.map(
+          (item) => {
+            return [Number(item.lat), (Number(item.lon || item.lng) + 360) % 360]
+          }
+        )
         // 请求成功后添加标记,并动态添加重置按钮
         addMarkersToMap()
+        if (data?.dottedLine) {
+          draw_marker(handleData(data.dottedLine), handleData(data.solidLine))
+          map.on('moveend', function () {
+            draw_marker(handleData(data.dottedLine), handleData(data.solidLine))
+            updateVisibleMarkers()
+          })
+          map.on('zoomend', function () {
+            draw_marker(handleData(data.dottedLine), handleData(data.solidLine))
+            updateVisibleMarkers()
+          })
+        }
       }
     })
 }
@@ -367,6 +530,10 @@ watch(
 onMounted(() => {
   initMap() // 初始化地图,不加标记
 })
+
+onUnmounted(() => {
+  map?.remove()
+})
 </script>
 
 <style lang="scss">
@@ -387,12 +554,13 @@ onMounted(() => {
           margin-left: -2px;
           font-size: 12px;
           font-weight: 700;
+          color: #2b2f36;
         }
       }
       .label {
         margin-bottom: 4px;
         font-size: 12px;
-        color: let(--color-neutral-2);
+        color: #646a73;
       }
     }
   }
@@ -401,19 +569,91 @@ onMounted(() => {
   .leaflet-popup-tip {
     display: none;
   }
-  .transport-map {
-    .leaflet-touch {
-      .leaflet-bar {
-        border: 0;
-        border-radius: 4px;
-        overflow: hidden;
+
+  .leaflet-control-zoom {
+    span {
+      color: #2b2f36;
+    }
+  }
+}
+.leaflet-control {
+  a.leaflet-control-zoom-in,
+  a.leaflet-control-zoom-out,
+  a.leaflet-bar-part {
+    width: 26px;
+    height: 26px;
+    font-size: 18px;
+  }
+}
+.dark-mode {
+  div.leaflet-control-zoom.leaflet-bar.leaflet-control {
+    border: 0;
+    border-radius: 4px;
+    box-shadow: none;
+    a {
+      background-color: #3c414a;
+      border-bottom: none;
+      span {
+        color: var(--color-neutral-1);
+      }
+      &:first-child {
+        span {
+          display: inline-block;
+          width: 24px;
+          border-bottom: 2px solid #575c64;
+        }
       }
     }
   }
+  .reset-zoom-control {
+    border: none;
+    background-color: #3c414a;
+  }
+  a.leaflet-bar-part {
+    background-color: #3c414a;
+    border-radius: 4px;
+    box-shadow: none;
+    overflow: hidden;
+    div {
+      border-color: var(--color-neutral-1) !important;
+      div {
+        background-color: var(--color-neutral-1) !important;
+      }
+    }
+  }
+}
+
+/* 示例:将所有地图图片的颜色反转 */
+.dark-mode img:not(.leaflet-marker-icon) {
+  filter: invert(1) hue-rotate(200deg);
+}
+// 防止暗黑模式下地图超出容器
+.tracking-map {
+  overflow: hidden;
+}
+// 修改暗黑模式下的背景色
+.leaflet-container.dark-mode,
+.leaflet-map-pane.dark-mode,
+.leaflet-tile-container.dark-mode {
+  background-color: #2b2f36;
+}
+// 处理版权信息在切换模式后样式错误bug
+.leaflet-right .leaflet-control-attribution {
+  background: rgba(255, 255, 255, 0.8);
+  margin: 0 4px 4px 0;
+  color: #2b2f36;
+  span {
+    font-size: 12px;
+    color: #2b2f36;
+  }
+  a {
+    color: #0078a8;
+    font-size: 12px;
+  }
 }
 /* 自定义重置缩放按钮控件样式 */
 .reset-zoom-control {
-  margin-top: 10px; /* 增加上边距,使按钮与默认缩放按钮之间有间距 */
+  margin-top: 10px;
   border-radius: 4px;
   background-color: white;
   border: 1px solid #ccc;
@@ -427,3 +667,66 @@ onMounted(() => {
   padding: 6px;
 }
 </style>
+
+<style lang="scss">
+.tracking-map {
+  .container {
+    position: relative;
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    height: 50px;
+    width: 50px;
+    /* background-color: #d9dddf; */
+    .font_family {
+      position: absolute;
+      top: 50%;
+      left: 50%;
+      transform: translate(-12px, -12px);
+      display: flex;
+      justify-content: center;
+      align-items: center;
+      width: 24px;
+      height: 24px;
+      padding-bottom: 2px;
+      padding-left: 1px;
+      color: white;
+      background-color: rgba(255, 117, 0, 1);
+      border-radius: 50%;
+      font-size: 14px;
+      border: 1px solid white;
+    }
+  }
+  .circle {
+    display: flex;
+    align-items: center;
+    justify-content: center;
+    width: 24px;
+    height: 24px;
+    background-color: rgba(255, 117, 0, 1);
+    border-radius: 50%;
+    position: relative;
+    animation: expandAndFade 1.3s linear infinite;
+    overflow: hidden; /* 确保子元素不会溢出 */
+
+    opacity: 0.9;
+  }
+
+  @keyframes expandAndFade {
+    0% {
+      transform: scale(1);
+      opacity: 0.9;
+    }
+
+    50% {
+      transform: scale(1.5);
+      opacity: 0.7;
+    }
+
+    100% {
+      transform: scale(2);
+      opacity: 0.2;
+    }
+  }
+}
+</style>

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

@@ -44,8 +44,9 @@ const handleTabClick = (name: string) => {
   width: 400px;
   height: 484px;
   background-color: #fff;
-  border: 1px solid #eaebed;
   border-radius: 12px;
+  border: 1px solid var(--color-border);
+  box-shadow: -2px 2px 12px 0px var(--color-shipment-status-shadow);
   .header {
     display: flex;
     height: 48px;

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

@@ -7,6 +7,9 @@ import { useRouter } from 'vue-router'
 import { ref, onMounted } from 'vue'
 import { transportationMode } from '@/components/TransportationMode'
 import { useLoadingState } from '@/stores/modules/loadingState'
+import { useThemeStore } from '@/stores/modules/theme'
+
+const themeStore = useThemeStore()
 
 const router = useRouter()
 const props = defineProps({
@@ -520,7 +523,11 @@ defineExpose({
     <div class="table-tools">
       <div class="left-total-records">{{ selectedNumber }} Selected</div>
       <div class="right-tools-btn">
-        <el-button class="el-button--main" @click="handleDownload">
+        <el-button
+          class="el-button--main"
+          :class="{ 'el-button--pain-theme': themeStore.theme === 'dark' }"
+          @click="handleDownload"
+        >
           <span style="margin-right: 8px" class="font_family icon-icon_download_b"></span>
           Download
         </el-button>