Forráskód Böngészése

feat:merge feature

AmandaG 9 hónapja
szülő
commit
4197126e8f

+ 30 - 0
src/api/module/tracking.ts

@@ -137,3 +137,33 @@ export const recordShareLinkClicked = (params: any, config: any) => {
     config
   )
 }
+
+/**
+ * 获取文件上传的类型
+ */
+export const getUploadType = (params: any, config: any) => {
+  return HttpAxios.post(
+    `${baseUrl}`,
+    {
+      action: 'ocean_order',
+      operate: 'document_upload',
+      ...params
+    },
+    config
+  )
+}
+
+/**
+ * 上传文件
+ */
+export const uploadFile = (params: any, config: any) => {
+  return HttpAxios.post(
+    `${baseUrl}`,
+    {
+      action: 'ocean_order',
+      operate: 'document_upload_do',
+      ...params
+    },
+    config
+  )
+}

+ 5 - 3
src/components/AutoSelect/src/AutoSelect.vue

@@ -28,6 +28,7 @@ const props = defineProps({
 interface ListItem {
   value: string
   label: string
+  checked: boolean
 }
 
 const list = ref<ListItem[]>([])
@@ -66,7 +67,7 @@ const remoteMethod = (query: string) => {
           loading.value = false
           if (res.code == 200) {
             list.value = res.data.map((item: any) => {
-              return { value: item, label: item }
+              return { value: item, label: item, checked: value.value?.includes(item) }
             })
             options.value = list.value.filter((item) => {
               return item.label.toLowerCase().includes(query.toLowerCase())
@@ -112,8 +113,9 @@ const removeClass = () => {
       :loading="loading"
     >
       <el-option v-for="item in options" :key="item.value" :label="item.label" :value="item.value">
-        <el-checkbox :checked="value?.includes(item.value)"></el-checkbox>
-        <div class="label">{{ item.value }}</div>
+        <el-checkbox :checked="item.checked">
+          <span class="label" @click="item.checked = !item.checked">{{ item.value }}</span>
+        </el-checkbox>
       </el-option>
     </el-select>
   </div>

+ 2 - 2
src/components/VLoading/src/VLoading.vue

@@ -46,11 +46,11 @@ const props = withDefaults(defineProps<internalProps>(), {
 }
 
 .v-loading-spinner {
+  position: sticky;
   top: 50%;
-  margin-top: -24px;
+  transform: translateY(-50%);
   width: 100%;
   text-align: center;
-  position: absolute;
 }
 
 .circular {

+ 8 - 3
src/components/selectAutoSelect/src/selectAutoSelect.vue

@@ -24,6 +24,7 @@ interface Props {
 interface optionsItem {
   value: string
   label: string
+  checked: boolean
 }
 
 const list = ref<ListItem[]>([])
@@ -92,9 +93,10 @@ const remoteMethod = (query: string) => {
         })
         .then((res: any) => {
           if (res.code == 200) {
+            console.log(testAuto.value)
             loading.value = false
             list.value = res.data.map((item: any) => {
-              return { value: item, label: item }
+              return { value: item, label: item, checked: testAuto.value?.includes(item) }
             })
             options.value = list.value.filter((item) => {
               return item.label.toLowerCase().includes(query.toLowerCase())
@@ -119,7 +121,9 @@ const changeSelect = (val: any) => {
 const emit = defineEmits(['changeAutoSelectAddType', 'delSelect', 'changeAutoSelect'])
 let AutoSelectObj: any = {}
 let AutoSelectObj2: any = {}
+const testAuto = ref()
 const changeAutoSelect = (val: any, value: any) => {
+  testAuto.value = value
   AutoSelectObj[val] = value.join()
   AutoSelectObj2[val] = value
   if (value.length) {
@@ -235,8 +239,9 @@ const typeSelectClick = (index: any, val: any) => {
           :label="item.label"
           :value="item.value"
         >
-          <el-checkbox :checked="AddType[index].partyname?.includes(item.value)"></el-checkbox>
-          <div class="label">{{ item.value }}</div>
+          <el-checkbox :checked="item.checked">
+            <span class="label" @click="item.checked = !item.checked">{{ item.value }}</span>
+          </el-checkbox>
         </el-option>
       </el-select>
       <div

+ 6 - 1
src/styles/elementui.scss

@@ -531,6 +531,11 @@ div .el-select-dropdown__item.is-hovering {
 .el-select-dropdown__item {
   border-radius: var(--border-radius-6);
   margin: 0 8px;
+
+  margin-bottom: 4px;
+  &:last-child {
+    margin-bottom: 0;
+  }
 }
 
 div .el-badge__content--warning {
@@ -792,4 +797,4 @@ div .DaterangeClass {
 }
 div .el-radio__label {
   width: 100%;
-}
+}

+ 8 - 0
src/styles/theme.scss

@@ -250,6 +250,10 @@
   --color-public-tracking-empty-bg: #fff;
   --color-dot-unchecked: #eeeeee;
   --color-dot-checked: #ed6d00;
+
+  --color-upload-file-bg: #fef8f2;
+  --color-upload-file-color: #b5b9bf;
+  --color-upload-file-border-bg: #f5b279;
 }
 
 :root.dark {
@@ -312,6 +316,10 @@
   --color-share-link-bg: #3a4149;
 
   --color-public-tracking-empty-bg: #2b2f36;
+
+  --color-upload-file-bg: rgba(237, 109, 0, 0.2);
+  --color-upload-file-color: rgba(240, 241, 243, 0.7);
+  --color-upload-file-border-bg: rgba(237, 109, 0, 0.5);
   // 滚动条
   --color-scrollbar-thumb: #656f7d;
 

+ 5 - 2
src/utils/table.ts

@@ -32,8 +32,7 @@ export const autoWidth = (tableData: VxeGridProps, grid: VxeGridInstance) => {
       })
       // column.title.length > curStr.length && (curStr = column.title)
       // 表头的宽度如果小于表格内容的宽度
-      if (width < curStr.length * 11 + 20) {
-        // width = curStr.length * 10 + 20
+      if (width < curStr.length * 11) {
         if (curStr.length > 20) {
           width = curStr.length * 9 + 40
         } else {
@@ -44,6 +43,10 @@ export const autoWidth = (tableData: VxeGridProps, grid: VxeGridInstance) => {
       // width < 100 && (width = 100)
       // 最终宽度不能超过400
       width > 400 && (width = 400)
+      // 如果字段是Mode,则固定宽度为80
+      if (field === 'Mode') {
+        width = 80
+      }
 
       columnsWidth.push({
         width,

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

@@ -413,7 +413,7 @@ const handleLinkClick = (row: any, column: any) => {
       query: { a: row.__serial_no, _schemas: row._schemas, status: row.Status }
     })
     visitedRowState.setBookingTableData(row['__serial_no'])
-  } else if (column.title === 'HBL No.') {
+  } else if (column.title === 'HBOL/HAWB No.') {
     router.push({
       path: '/tracking/detail',
       query: { a: row.__serial_no, _schemas: row._schemas }

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

@@ -257,6 +257,7 @@ const SubscribeShipments = () => {
         :_schemas="allData?._schemas"
         :serial_no="allData?.serial_no"
         :uncode="allData?.uncode"
+        :mode="allData?.transportInfo?.mode"
       ></MapView>
       <TransportStep class="transport-step" :data="allData"></TransportStep>
     </div>
@@ -332,7 +333,7 @@ const SubscribeShipments = () => {
             <VBox :id="item.id" :isSeeAll="false" @draggable="handleDraggable">
               <template #header>Attachment</template>
               <template #content>
-                <AttachmentView :data="allData"></AttachmentView>
+                <AttachmentView @update="getData" :data="allData"></AttachmentView>
               </template>
             </VBox>
           </div>

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

@@ -97,14 +97,19 @@ const uploadFilesRef = ref<InstanceType<typeof UploadFilesDialog> | null>(null)
 const openUploadFilesDialog = () => {
   uploadFilesRef.value?.openDialog()
 }
+
+const emit = defineEmits<{ update: [] }>()
+const updateData = () => {
+  emit('update')
+}
 </script>
 
 <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> -->
+      <span>Upload Files</span>
+    </el-button>
     <vxe-grid class="radius-bottom" ref="tableRef" v-bind="tableData">
       <template #action="{ row }">
         <el-button @click="handleDownload(row)" class="el-button--icon">
@@ -126,7 +131,12 @@ const openUploadFilesDialog = () => {
         <div class="empty">No data</div>
       </template>
     </vxe-grid>
-    <UploadFilesDialog ref="uploadFilesRef"></UploadFilesDialog>
+    <UploadFilesDialog
+      v-if="props.data"
+      :data="props.data"
+      @update="updateData"
+      ref="uploadFilesRef"
+    ></UploadFilesDialog>
   </div>
 </template>
 

+ 449 - 28
src/views/Tracking/src/components/TrackingDetail/src/components/MapView.vue

@@ -5,6 +5,7 @@
     style="width: 100%; height: 448px"
     class="tracking-map"
     :class="{ 'dark-mode': themeStore.theme === 'dark' }"
+    v-vloading="loading"
   ></div>
 </template>
 <script setup lang="ts">
@@ -12,8 +13,9 @@ import L from 'leaflet'
 import DestinationIcon from '../images/destinationIcon.png'
 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 { useThemeStore } from '@/stores/modules/theme'
+import { transportationMode } from '@/components/TransportationMode'
 
 const themeStore = useThemeStore()
 
@@ -21,6 +23,7 @@ const props = defineProps<{
   serial_no?: string
   uncode?: string
   _schemas?: string
+  mode?: string
 }>()
 
 const markerPositions = ref([])
@@ -63,6 +66,179 @@ const initMap = () => {
   }).addTo(map)
 }
 
+// 修正经度
+const fixLng = (lng: number) => {
+  while (lng > 180) lng = lng - 360
+  while (lng <= -180) lng = lng + 360
+  return lng
+}
+const getBBox = () => {
+  let bnd = map.getBounds()
+  let ww = bnd.getEast() - bnd.getWest()
+  let cent_pt = map.getCenter()
+  let bbox = [
+    fixLng(cent_pt.lng - ww / 2),
+    bnd.getSouth(),
+    fixLng(cent_pt.lng + ww / 2),
+    bnd.getNorth()
+  ]
+
+  let bbox2 = []
+  let bbox3 = []
+  if (ww > 360) {
+    bbox[0] = -180
+    bbox[2] = 0
+
+    bbox2[0] = 0
+    bbox2[1] = bbox[1]
+    bbox2[2] = 180
+    bbox2[3] = bbox[3]
+  }
+  if (bbox[2] * bbox[0] < 0 || bbox[2] < bbox[0]) {
+    if (bbox[0] >= 0) {
+      bbox2[0] = -180
+      bbox2[2] = bbox[2]
+      bbox2[1] = bbox[1]
+      bbox2[3] = bbox[3]
+      bbox[2] = 180
+    } else {
+      bbox2[0] = 0
+      bbox2[2] = bbox[2]
+      bbox2[1] = bbox[1]
+      bbox2[3] = bbox[3]
+      bbox[2] = 0
+    }
+  }
+  if (bbox2[2] * bbox2[0] < 0 || bbox2[2] < bbox2[0]) {
+    if (bbox2[0] >= 0) {
+      bbox3[0] = -180
+      bbox3[2] = bbox2[2]
+      bbox3[1] = bbox2[1]
+      bbox3[3] = bbox2[3]
+      bbox2[2] = 180
+    } else {
+      bbox3[0] = 0
+      bbox3[2] = bbox2[2]
+      bbox3[1] = bbox2[1]
+      bbox3[3] = bbox2[3]
+      bbox2[2] = 0
+    }
+  }
+  let ll = []
+  ll.push(bbox)
+
+  if (bbox2.length > 0) ll.push(bbox2)
+  if (bbox3.length > 0) ll.push(bbox3)
+  return ll
+}
+let track_added_marker = []
+
+const clear_marker = () => {
+  track_added_marker.forEach((v) => {
+    map!.removeLayer(v)
+  })
+
+  track_added_marker = []
+}
+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 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 lines_list = [mpts]
+  while (kk >= cc1 && kk <= cc2 && cc > 0) {
+    lines_list.forEach((a) => {
+      let ii = 0
+      let jj = 0
+      let pts = []
+      for (ii = 0; ii < a.length; ii++) pts[jj++] = [a[ii][0], a[ii][1] + kk * 360]
+
+      if (jj > 0) {
+        if (IsDash) {
+          showTrackLine(
+            pts,
+            jj,
+            Object.assign({}, { color: '#ff7500', dashArray: '10', weight: 2 }, opts)
+          )
+        } else {
+          showTrackLine(pts, jj, Object.assign({}, { color: '#ff7500', weight: 1 }, opts))
+        }
+      }
+    })
+
+    kk++
+    cc--
+  }
+}
+
+const showTrackLine = (pts, jj, opts) => {
+  let arrow = L.polyline(pts, Object.assign({}, { color: '#ff7500', weight: 2 }, opts)).addTo(map)
+  track_added_marker.push(arrow)
+}
+
 const addResetZoomButton = (center: L.LatLng, zoom: number) => {
   const ResetZoomControl = L.Control.extend({
     options: {
@@ -98,9 +274,80 @@ let initialCenter: L.LatLng | null = null
 let initialZoomLevel: number | null = null
 let isFirstRender = true // 标记是否为首次渲染
 
-let allMarkers = []
+let mirrorMarkers = ref({})
 
-// 添加标记后更新中心和缩放级别
+// 设置中心点之后的东北角和西南角的经纬度对象
+let latLngBoundsObj = ref({ northEast: { lat: 0, lng: 0 }, southWest: { lat: 0, lng: 0 } })
+// 设置中心和缩放级别
+const setCenterAndZoom = () => {
+  if (viewData.value.length > 0) {
+    // 根据标记的位置设置中心点以及缩放级别
+    const bounds = L.latLngBounds(viewData.value)
+    map!.fitBounds(bounds, { paddingTopLeft: [20, 90], paddingBottomRight: [0, 30] })
+
+    setTimeout(() => {
+      if (isFirstRender) {
+        initialCenter = map!.getCenter()
+        initialZoomLevel = map!.getZoom()
+        isFirstRender = false
+        // 定义最小缩放等级  缩放等级数值越小,地图显示的区域越大
+        const minZoom = 5 // 示例:最小缩放等级为 5
+
+        // 如果当前缩放等级大于最小缩放等级,则调整缩放等级
+        if (initialZoomLevel > minZoom) {
+          map!.setZoom(minZoom)
+          initialZoomLevel = minZoom
+        }
+      }
+      // 获取东北角和西南角的经纬度
+      const curLatLon = map.getBounds()
+      latLngBoundsObj.value = {
+        northEast: { lat: curLatLon.getNorth(), lng: curLatLon.getEast() },
+        southWest: { lat: curLatLon.getSouth(), lng: curLatLon.getWest() }
+      }
+      addResetZoomButton(initialCenter!, initialZoomLevel!)
+    }, 500)
+  }
+}
+
+// 用来记录初始化时默认打开的弹出窗口,以便在地图移动时判断是否需要重新打开弹出窗口
+const popupObj = ref({})
+// 将经纬度格式化为四位小数,防止精度问题
+const formatLocationNum = (num: string | number) => {
+  return Number(num).toFixed(4)
+}
+// 判断是否需要打开弹出窗口
+const shouldDisplayPopup = (isInit?: boolean) => {
+  const allMarkers = {
+    ...initMarksObj.value,
+    ...mirrorMarkers.value
+  }
+  Object.values(allMarkers).forEach((marker: any) => {
+    // 创建 LatLngBounds 对象
+    const bounds = L.latLngBounds(latLngBoundsObj.value.southWest, latLngBoundsObj.value.northEast)
+    const marksLatLng = marker.getLatLng()
+    // 检查标记是否在边界内
+    const isInsideBounds = bounds.contains(marksLatLng)
+    const locationKey = `${formatLocationNum(marksLatLng.lat)},${formatLocationNum(marksLatLng.lng)}`
+    if (!isInsideBounds) return
+    if (isInit) {
+      if (!popupObj.value[locationKey]) {
+        popupObj.value[locationKey] = true
+        marker.openPopup()
+      }
+    } else {
+      const popupItem = popupObj.value[locationKey]
+      // 如果用户没有关闭弹出窗口,则重新打开弹出窗口
+      if (popupItem) {
+        marker.openPopup()
+      }
+    }
+  })
+}
+
+// 初始标记的位置
+const initMarksObj = ref({})
+// 添加标记
 const addMarkersToMap = () => {
   if (!map) return // 确保地图已经初始化
 
@@ -116,35 +363,184 @@ const addMarkersToMap = () => {
         </p>
       </div>
     `
-    marker
-      .bindPopup(customPopupContent, {
-        closeButton: false,
-        autoClose: false,
-        closeOnClick: false
-      })
-      .openPopup()
-    allMarkers[`${position.lat},${position.lng}`] = marker
+    marker.bindPopup(customPopupContent, {
+      closeButton: false,
+      autoClose: false,
+      autoPan: false, // 防止弹出框打开时移动地图
+      closeOnClick: false
+    })
+    marker.on('click', () => {
+      const latLng = marker.getLatLng() // 获取标记的经纬度
+      const key = `${latLng.lat},${latLng.lng}` // 使用经纬度组合成一个唯一键
+      if (key in popupObj.value) {
+        popupObj.value[key] = marker.isPopupOpen() // 直接切换 isPopup 状态
+      }
+    })
+    initMarksObj.value[`${position.lat},${position.lng}`] = marker
   })
+  setTimeout(() => {
+    updateVisibleMarkers(true)
+  }, 1000)
+}
 
-  if (viewData.value.length > 0) {
-    // 根据标记的位置设置中心点以及缩放级别
-    const bounds = L.latLngBounds(viewData.value)
-    map!.fitBounds(bounds, { paddingTopLeft: [20, 70], paddingBottomRight: [0, 0] })
-    setTimeout(() => {
-      if (isFirstRender) {
-        initialCenter = map!.getCenter()
-        initialZoomLevel = map!.getZoom()
-        isFirstRender = false
+// 新增轮船当前位置标记
+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-${transportationMode?.[props?.mode]}" ></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]
+    // 计算线段末端的角度(以弧度为单位)
+    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 = (isInit?: boolean) => {
+  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
+
+  // 移除所有标记
+  Object.values(mirrorMarkers.value).forEach((marker: any) => {
+    map.removeLayer(marker)
+    delete mirrorMarkers.value[`${marker.getLatLng().lat},${marker.getLatLng().lng}`]
+  })
+
+  // 计算当前视图中的标记,包括多地球的情况
+  while (x >= cc1 && x <= cc2 && cc > 0) {
+    if (x === 0) {
+      // 初始化时不添加当前轮船位置标记
+      if (!isInit) addShipMarker(x)
+      x++
+      cc--
+      continue
+    }
+
+    Object.values(initMarksObj.value).forEach((marker: any) => {
+      const latLng = marker.getLatLng()
+      const key = `${latLng.lat},${latLng.lng + x * 360}`
+      if (!mirrorMarkers.value[key]) {
+        const newMarker: any = L.marker([latLng.lat, latLng.lng + x * 360], {
+          icon: marker.options.icon
+        }).bindPopup(marker.getPopup().getContent(), marker.getPopup().options)
+
+        newMarker.on('click', () => {
+          const latLng = newMarker.getLatLng() // 获取标记的经纬度
+          const key = `${latLng.lat},${latLng.lng}` // 使用经纬度组合成一个唯一键
+
+          if (key in popupObj.value) {
+            popupObj.value[key] = newMarker.isPopupOpen() // 直接切换 isPopup 状态
+          }
+        })
+        mirrorMarkers.value[key] = newMarker
+        map.addLayer(newMarker)
       }
-      addResetZoomButton(initialCenter!, initialZoomLevel!)
-    }, 500)
+      newVisibleMarkers.add(mirrorMarkers.value[key])
+    })
+
+    addShipMarker(x)
+
+    x++
+    cc--
   }
+  shouldDisplayPopup(isInit)
+}
+
+// 处理得到的数据
+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 loading = ref(false)
 // 请求接口并处理标记
 const getMarker = () => {
+  loading.value = true
   $api
     .getTrackingDetailMapData({
       serial_no: props.serial_no,
@@ -155,8 +551,8 @@ const getMarker = () => {
       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 },
@@ -171,12 +567,37 @@ const getMarker = () => {
               iconColor: iconColorList[item.label].color
             })
           })
-        viewData.value = data?.map((item) => {
-          return [Number(item.lat), Number(item.lng)]
-        }) // 请求成功后添加标记,并动态添加重置按钮
+        if (data?.rangePoint.length > 0) {
+          if (Number(data.rangePoint[2].lon) < Number(data.rangePoint[1].lon)) {
+            data.rangePoint[2].lon = Number(data.rangePoint[2].lon) + 360
+          }
+          data.rangePoint.splice(0, 1)
+        }
+        viewData.value = (data?.rangePoint.length > 0 ? data?.rangePoint : data?.point)?.map(
+          (item) => {
+            return { lat: Number(item.lat), lng: Number(item.lon || item.lng) }
+          }
+        )
+        // 请求成功后添加标记
         addMarkersToMap()
+        // 设置中心和缩放级别
+        setCenterAndZoom()
+        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()
+          })
+        }
       }
     })
+    .finally(() => {
+      loading.value = false
+    })
 }
 
 // 监听 `serial_no` 变化
@@ -405,4 +826,4 @@ onUnmounted(() => {
     }
   }
 }
-</style>
+</style>

+ 116 - 30
src/views/Tracking/src/components/TrackingDetail/src/components/UploadFilesDialog.vue

@@ -1,22 +1,37 @@
 <script setup lang="ts">
+const props = defineProps({
+  data: Object
+})
+
 const dialogVisible = ref(false)
 const openDialog = () => {
   dialogVisible.value = true
 }
 
-const fileType = ref('Certificate of Origin')
+const fileType = ref('')
 defineExpose({
   openDialog
 })
 
-const fileTypeList = ['Certificate of Origin', 'House Bill of Lading', 'E-Packing List']
-
-const uploadFileList = ref()
+const uploadFileList = ref([])
 
 const changeFileList = (file: any, fileList: any) => {
+  if (file.size / 1024 / 1024 > 5) {
+    const index = fileList.findIndex((item: any) => item.uid === file.uid)
+    if (index !== -1) {
+      fileList.splice(index, 1)
+    }
+    ElMessage.warning('File size must not exceed 5MB!')
+    return false
+  }
   // 给当前文件添加fileType属性
   file.raw.fileType = fileType.value
   uploadFileList.value = fileList
+  if (fileList.length >= 5) {
+    disableUpload.value = true
+  } else {
+    disableUpload.value = false
+  }
 }
 
 function bytesToKB(bytes: number) {
@@ -28,8 +43,39 @@ const removeFile = (file: any) => {
   uploadRef.value.handleRemove(file)
 }
 
+const finishLoading = ref(false)
+const emit = defineEmits<{ update: [] }>()
 const handleSave = () => {
-  uploadRef.value.submit()
+  // uploadRef.value.submit()
+  if (fileTypeList.value.length === 0 || uploadFileList.value.length === 0) {
+    ElMessage.warning('Please select file type and upload file')
+    return
+  }
+
+  if (finishLoading.value) return
+  finishLoading.value = true
+  const fileList = uploadFileList.value.map((item: any) => item.raw)
+  return $api
+    .uploadFile({
+      file: fileList,
+      file_type: fileType.value,
+      transport_mode: props.data?.transport_mode,
+      _schemas: props.data?._schemas,
+      h_bol: props.data?.basicInfo?.['HAWB/HBOL']
+    })
+    .then((res: any) => {
+      if (res.code === 200) {
+        ElMessage.success('Upload successfully')
+        emit('update')
+        dialogVisible.value = false
+      }
+    })
+    .catch(() => {
+      ElMessage.error('Upload failed')
+    })
+    .finally(() => {
+      finishLoading.value = false
+    })
 }
 
 // 文件上传进度处理
@@ -52,6 +98,23 @@ const handleSuccess = (response: any, file: any) => {
   }
 }
 
+const fileTypeList = ref([])
+// 获取上传文件类型
+const getFileType = () => {
+  $api
+    .getUploadType({
+      transport_mode: props.data?.transport_mode,
+      _schemas: props.data?._schemas
+    })
+    .then((res: any) => {
+      fileTypeList.value = res.data?.['File Type']
+      fileType.value = fileTypeList.value[0]?.file_type
+    })
+}
+onMounted(() => {
+  getFileType()
+})
+
 const clearData = () => {
   uploadFileList.value = []
   uploadRef.value.clearFiles()
@@ -59,19 +122,23 @@ const clearData = () => {
 const beforeAvatarUpload = (rawFile: any) => {
   if (
     ![
-      'application/pdf',
-      'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
-      'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
+      '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.')
+    // , DOCX, and XLSX
+    ElMessage.error('The file types allowed for upload are: PDF.')
     return false
-  } else if (rawFile.size / 1024 / 1024 > 25) {
-    ElMessage.error('File size must not exceed 25MB!')
+  } else if (rawFile.size / 1024 / 1024 > 5) {
+    ElMessage.error('File size must not exceed 5MB!')
     return false
   }
   return true
 }
+const url = `${import.meta.env.VITE_API_HOST}/main_new_version.php`
+
+const disableUpload = ref(false)
 </script>
 
 <template>
@@ -84,18 +151,25 @@ const beforeAvatarUpload = (rawFile: any) => {
   >
     <div class="upload-template">
       <div class="label">File Type</div>
-      <el-select v-model="fileType" placeholder="string">
-        <el-option v-for="item in fileTypeList" :key="item" :label="item" :value="item"></el-option>
+      <el-select v-model="fileType" placeholder="file type">
+        <el-option
+          v-for="item in fileTypeList"
+          :key="item.file_type"
+          :label="item.file_type"
+          :value="item.file_type"
+        ></el-option>
       </el-select>
+      <!-- ,.docx,.xlsx -->
       <el-upload
         class="upload-demo"
         ref="uploadRef"
-        :auto-upload="false"
         drag
-        :accept="'application/pdf,.docx,.xlsx'"
+        :limit="5"
+        :accept="'application/pdf'"
         :show-file-list="false"
-        :action="'http://localhost:3000/upload'"
-        multiple
+        :action="url"
+        :auto-upload="false"
+        :disabled="disableUpload || fileTypeList.length === 0"
         :before-upload="beforeAvatarUpload"
         @change="changeFileList"
         @remove="changeFileList"
@@ -106,10 +180,13 @@ const beforeAvatarUpload = (rawFile: any) => {
           <span class="font_family icon-icon_upload_b"></span> <span>Upload</span>
         </div>
         <div class="el-upload-tips">
-          <span class="font_family icon-icon_info_b"></span>
-          <span style="font-size: 12px"
-            >Supported formats: pdf, docx, xlsx ; Maximum Size: 25MB</span
-          >
+          <div class="label">
+            <span class="font_family icon-icon_info_b" style="vertical-align: baseline"></span>
+            <!-- , docx, xlsx  -->
+            <span>Supported formats: pdf ; </span>
+          </div>
+          <span>Maximum Size: 5MB; </span>
+          <span>Maximum Number: 5 files</span>
         </div>
       </el-upload>
     </div>
@@ -123,12 +200,13 @@ const beforeAvatarUpload = (rawFile: any) => {
         <div class="file">
           <span class="font_family icon-icon_edoc_b"></span>
           <span class="file-name">{{ item.name }}</span>
-          <span class="size">{{
+          <!-- <span class="size">{{
             item.percentage !== undefined ? item.percentage + '%' : bytesToKB(item.size) + 'KB'
-          }}</span>
+          }}</span> -->
+          <span class="size">{{ bytesToKB(item.size) + 'KB' }}</span>
         </div>
         <!-- 添加进度条 -->
-        <el-progress
+        <!-- <el-progress
           style="width: 100%"
           v-if="item.percentage !== undefined"
           :percentage="item.percentage"
@@ -136,7 +214,7 @@ const beforeAvatarUpload = (rawFile: any) => {
           color="#ed6d00"
           status="success"
           :stroke-width="4"
-        />
+        /> -->
       </div>
     </el-scrollbar>
     <template #footer>
@@ -145,7 +223,7 @@ const beforeAvatarUpload = (rawFile: any) => {
           style="height: 40px; padding: 8px 40px"
           class="download-btn el-button--dark"
           @click="handleSave"
-          >Finish</el-button
+          >Upload</el-button
         >
       </div>
     </template>
@@ -169,8 +247,8 @@ const beforeAvatarUpload = (rawFile: any) => {
   :deep(.el-upload-dragger) {
     padding-top: 19px;
     padding-bottom: 19px;
-    background-color: #fef8f2;
-    border: 1px dashed #f5b279;
+    background-color: var(--color-upload-file-bg);
+    border: 1px dashed var(--color-upload-file-border-bg);
   }
   .el-upload-text {
     margin-bottom: 8px;
@@ -184,8 +262,15 @@ const beforeAvatarUpload = (rawFile: any) => {
       margin-right: 2px;
       transform: translateY(2px);
     }
-    span {
-      color: var(--color-neutral-3);
+    span,
+    p {
+      vertical-align: middle;
+      font-size: 12px;
+      color: var(--color-upload-file-color);
+    }
+    .label {
+      height: 16px;
+      line-height: 16px;
     }
   }
 }
@@ -197,6 +282,7 @@ const beforeAvatarUpload = (rawFile: any) => {
   .file-item {
     padding: 10px 8px;
     margin-bottom: 8px;
+    border-radius: 6px;
     background-color: var(--color-header-bg);
     & > .header {
       margin-bottom: 8px;

+ 2 - 2
src/views/Tracking/src/components/TrackingTable/src/TrackingTable.vue

@@ -491,7 +491,7 @@ const handleLinkClick = (row: any, column: any) => {
       path: '/booking/detail',
       query: { a: row.__serial_no, _schemas: row._schemas, status: row.Status }
     })
-  } else if (column.title === 'HBL No.') {
+  } else if (column.title === 'HBOL/HAWB No.') {
     router.push({
       path: '/tracking/detail',
       query: { a: row.__serial_no, _schemas: row._schemas }
@@ -622,7 +622,7 @@ defineExpose({
       <!-- action操作的插槽 -->
       <template #action="{ row }">
         <el-button
-          v-if="row?.['Transportation Mode'] !== 'Air Freight'"
+          v-if="row?.['Mode'] !== 'Air Freight'"
           @click="handleVGM(row)"
           class="el-button--blue"
           style="height: 24px"