#441 实现首页KAM Mapping功能

Gabung
JackZ menggabungkan 116 komit dari United_Software/dev_zyh menjadi United_Software/dev%! (template.HTML=5 hari lalu)s
89 mengubah file dengan 7046 tambahan dan 6249 penghapusan
  1. 16 0
      src/api/module/Delivery.ts
  2. 16 0
      src/api/module/report.ts
  3. 50 66
      src/components/AddRules/src/AddRules.vue
  4. 77 54
      src/components/AutoSelect/src/AutoSelect.vue
  5. 21 22
      src/components/CreateAddRules/src/CreateAddRules.vue
  6. 3 0
      src/components/CustomizeColumns/src/CustomizeColumns.vue
  7. 115 248
      src/components/DateRange/src/DateRange.vue
  8. 3 0
      src/components/DateRange/src/components/QuickCalendarDate.vue
  9. 172 0
      src/components/DateRange/src/components/VCalendarDate.vue
  10. 51 66
      src/components/FliterTags/src/FilterTags.vue
  11. 151 975
      src/components/MoreFilters/src/MoreFilters.vue
  12. 237 0
      src/components/MoreFilters/src/components/PartiesView.vue
  13. 230 0
      src/components/MoreFilters/src/components/PlacesView.vue
  14. 130 97
      src/components/MoreFilters/src/components/SelectValue.vue
  15. 0 114
      src/components/SelectTable/src/SelectTable copy.vue
  16. 172 156
      src/components/SelectTable/src/SelectTable.vue
  17. 57 152
      src/components/SelectTableSelect/src/SelectTableSelect.vue
  18. 49 175
      src/components/TransportMode/src/TransportMode.vue
  19. 2 2
      src/components/VBreadcrumb/src/VBreadcrumb.vue
  20. 10 10
      src/components/VTag/src/VTag.vue
  21. 175 194
      src/components/selectAutoSelect/src/selectAutoSelect.vue
  22. 3 8
      src/router/index.ts
  23. 18 10
      src/stores/modules/breadCrumb.ts
  24. 115 0
      src/stores/modules/filtersList.ts
  25. 0 18
      src/stores/modules/loadingState.ts
  26. 65 27
      src/styles/Antdui.scss
  27. 60 4
      src/styles/icons/iconfont.css
  28. 0 0
      src/styles/icons/iconfont.js
  29. 2 0
      src/styles/icons/iconfont.svg
  30. TEMPAT SAMPAH
      src/styles/icons/iconfont.ttf
  31. TEMPAT SAMPAH
      src/styles/icons/iconfont.woff
  32. TEMPAT SAMPAH
      src/styles/icons/iconfont.woff2
  33. 66 2
      src/styles/theme.scss
  34. 32 16
      src/utils/axios.ts
  35. 41 42
      src/utils/table.ts
  36. 18 5
      src/utils/tools.ts
  37. 144 393
      src/views/Booking/src/BookingView.vue
  38. 4 2
      src/views/Booking/src/components/BookingDetail/src/components/EmailView.vue
  39. 15 9
      src/views/Booking/src/components/BookingTable/src/BookingTable.vue
  40. 18 4
      src/views/Booking/src/components/BookingTable/src/components/DownloadDialog.vue
  41. 418 418
      src/views/Dashboard/src/DashboardView.vue
  42. 235 229
      src/views/Dashboard/src/components/BarChart.vue
  43. 196 0
      src/views/Dashboard/src/components/CustomerFilter.vue
  44. 92 90
      src/views/Dashboard/src/components/DashFiters.vue
  45. 7 1
      src/views/Dashboard/src/components/PieChart.vue
  46. 1 1
      src/views/Dashboard/src/components/RecentStatus.vue
  47. 124 12
      src/views/Dashboard/src/components/RevenueChart.vue
  48. 256 218
      src/views/Dashboard/src/components/SellerChart.vue
  49. TEMPAT SAMPAH
      src/views/Dashboard/src/image/xiazai.png
  50. 78 263
      src/views/DestinationDelivery/src/DestinationDelivery.vue
  51. 263 0
      src/views/DestinationDelivery/src/components/CalendarTagDetailDialog.vue
  52. 530 0
      src/views/DestinationDelivery/src/components/CalendarView.vue
  53. 24 12
      src/views/DestinationDelivery/src/components/ConfiguRations/src/components/CreateNewRule.vue
  54. 192 107
      src/views/DestinationDelivery/src/components/CreateNewBooking/src/CreateNewbooking.vue
  55. 34 13
      src/views/DestinationDelivery/src/components/CreateNewBooking/src/components/NewbookingTable.vue
  56. 1 1
      src/views/DestinationDelivery/src/components/DeliveryDate.vue
  57. 294 0
      src/views/DestinationDelivery/src/components/ListView.vue
  58. 0 1
      src/views/DestinationDelivery/src/components/ModifyBooking/index.ts
  59. 0 275
      src/views/DestinationDelivery/src/components/ModifyBooking/src/ModifyBooking.vue
  60. 0 152
      src/views/DestinationDelivery/src/components/ModifyBooking/src/components/SelectShipmentsTable.vue
  61. 5 17
      src/views/DestinationDelivery/src/components/TableView/src/TableView.vue
  62. 112 48
      src/views/DestinationDelivery/src/components/TableView/src/components/ShipmentInforTable.vue
  63. 16 30
      src/views/Layout/src/components/Header/HeaderView.vue
  64. 113 0
      src/views/Layout/src/components/Header/components/KAMMapping.vue
  65. 1 1
      src/views/Layout/src/components/Header/components/LogoutDialog.vue
  66. 81 82
      src/views/Layout/src/components/Menu/MenuView.vue
  67. 10 2
      src/views/Login/src/loginView.vue
  68. 183 81
      src/views/PromptConfiguration/src/PromptConfiguration.vue
  69. 96 48
      src/views/PromptConfiguration/src/components/OutputConfiguration.vue
  70. 39 21
      src/views/Report/src/components/ReportDetail/src/ReportDetail.vue
  71. 41 25
      src/views/Report/src/components/ReportDetail/src/components/FieldsTable.vue
  72. 3 1
      src/views/Report/src/components/ReportSchedule/src/ReportSchedule.vue
  73. 1 0
      src/views/Report/src/components/ReportSchedule/src/components/FieldsTable.vue
  74. 104 7
      src/views/TemplateManagement/src/TemplateManagement.vue
  75. 49 448
      src/views/TemplateManagement/src/components/CreateReportTemplate/src/CreateReportTemplate.vue
  76. 4 1
      src/views/TemplateManagement/src/components/CreateReportTemplate/src/components/AccountSelect.vue
  77. 8 4
      src/views/TemplateManagement/src/components/CreateReportTemplate/src/components/AdjustmentField.vue
  78. 4 8
      src/views/TemplateManagement/src/components/CreateReportTemplate/src/components/PartyIDSelect.vue
  79. 733 0
      src/views/TemplateManagement/src/components/CreateReportTemplate/src/components/ReportFieldsConfiguration.vue
  80. 16 6
      src/views/TemplateManagement/src/components/TableView/src/TableView.vue
  81. 214 673
      src/views/Tracking/src/TrackingView.vue
  82. 1 1
      src/views/Tracking/src/components/PublicTracking/src/components/PublicTrackingDetail.vue
  83. 1 1
      src/views/Tracking/src/components/TrackingDetail/src/TrackingDetail.vue
  84. 4 2
      src/views/Tracking/src/components/TrackingDetail/src/components/EmailDrawer.vue
  85. 6 6
      src/views/Tracking/src/components/TrackingDetail/src/components/UploadFilesDialog.vue
  86. 21 65
      src/views/Tracking/src/components/TrackingTable/src/TrackingTable.vue
  87. 23 5
      src/views/Tracking/src/components/TrackingTable/src/components/DownloadDialog.vue
  88. 2 2
      tsconfig.app.json
  89. 72 0
      vite.config.ts.timestamp-1772161604204-11dfcc4937af7.mjs

+ 16 - 0
src/api/module/Delivery.ts

@@ -333,4 +333,20 @@ export const downloadBookingTableFile = (params: any, config: any) => {
     },
     config
   )
+}
+
+
+/**
+ * 获取delivery calendar view日历数据
+ */
+export const getDeliveryCalendarData = (params: any, config: any) => {
+  return HttpAxios.post(
+    `${baseUrl}`,
+    {
+      action: 'destination_delivery_booking',
+      operate: 'search_calendar',
+      ...params
+    },
+    config
+  )
 }

+ 16 - 0
src/api/module/report.ts

@@ -191,6 +191,22 @@ export const getReportDetailTable = (params: any, config: any) => {
   )
 }
 
+
+/**
+ * 获取Report detail页筛选项配置数据
+ */
+export const getReportDetailFilterConfig = (params: any, config: any) => {
+  return HttpAxios.post(
+    `${baseUrl}`,
+    {
+      action: 'shipment_status_report',
+      operate: 'report_detail_column',
+      ...params
+    },
+    config
+  )
+}
+
 /**
  * 获取导出Report detail页表格数据
  */

+ 50 - 66
src/components/AddRules/src/AddRules.vue

@@ -388,8 +388,7 @@ const Savesubscribe = () => {
   if (props.TitleType == 'Milestone') {
     savesubscribeobj.rules_type = 'Milestone_Update'
     if (
-      OceanCheckList.value.length == 0 &&
-      AirCheckList.value.length == 0 ||
+      (OceanCheckList.value.length == 0 && AirCheckList.value.length == 0) ||
       MilFrequencyList.value.length == 0 ||
       MilMethodsList.value == undefined ||
       MilMethodsList.value.length == 0
@@ -408,21 +407,17 @@ const Savesubscribe = () => {
     } else {
       savesubscribeobj.ocean_milestone = OceanCheckCode.value
       savesubscribeobj.air_milestone = AirCheckListCode.value
-      if(OceanCheckList.value.length == 0) {
-        str =
-        'Air Milestones: ' +
-        AirCheckList.value.join(',')
-      } else if(AirCheckList.value.length == 0) {
-        str =
-        'Ocean Milestones: ' +
-        OceanCheckList.value.join(',')
+      if (OceanCheckList.value.length == 0) {
+        str = 'Air Milestones: ' + AirCheckList.value.join(',')
+      } else if (AirCheckList.value.length == 0) {
+        str = 'Ocean Milestones: ' + OceanCheckList.value.join(',')
       } else {
         str =
-        'Ocean Milestones: ' +
-        OceanCheckList.value.join(',') +
-        ';\nAir Milestones: ' +
-        AirCheckList.value.join(',') +
-        ';'
+          'Ocean Milestones: ' +
+          OceanCheckList.value.join(',') +
+          ';\nAir Milestones: ' +
+          AirCheckList.value.join(',') +
+          ';'
       }
       savesubscribeobj.event_details = str
       SaveSuceessful()
@@ -455,8 +450,7 @@ const Savesubscribe = () => {
   } else if (props.TitleType == 'Departure') {
     savesubscribeobj.rules_type = 'Departure/Arrival_Delay'
     if (
-      DelayedDeparturedList.value.length == 0 &&
-      DelayedAirdList.value.length == 0 ||
+      (DelayedDeparturedList.value.length == 0 && DelayedAirdList.value.length == 0) ||
       DepFrequencyList.value.length == 0 ||
       DepMethodsList.value == undefined ||
       DepMethodsList.value.length == 0
@@ -473,21 +467,17 @@ const Savesubscribe = () => {
       missingmessage.value = missingmessage.value.substring(0, missingmessage.value.length - 2)
       UnableSaveVisible.value = true
     } else {
-      if(DelayedDeparturedList.value.length == 0) {
-        str =
-        'Air: ' +
-        DelayedAirdList.value.join(',')
-      } else if(DelayedAirdList.value.length == 0) {
-        str =
-        'Ocean: ' +
-        DelayedDeparturedList.value.join(',')
+      if (DelayedDeparturedList.value.length == 0) {
+        str = 'Air: ' + DelayedAirdList.value.join(',')
+      } else if (DelayedAirdList.value.length == 0) {
+        str = 'Ocean: ' + DelayedDeparturedList.value.join(',')
       } else {
         str =
-        'Ocean: ' +
-        DelayedDeparturedList.value.join(',') +
-        ';\nAir: ' +
-        DelayedAirdList.value.join(',') +
-        ';'
+          'Ocean: ' +
+          DelayedDeparturedList.value.join(',') +
+          ';\nAir: ' +
+          DelayedAirdList.value.join(',') +
+          ';'
       }
       savesubscribeobj.event_details = str
       SaveSuceessful()
@@ -495,8 +485,7 @@ const Savesubscribe = () => {
   } else {
     savesubscribeobj.rules_type = 'ETD/ETA_Change'
     if (
-      ETDOceanList.value.length == 0 &&
-      ETDAirList.value.length == 0 ||
+      (ETDOceanList.value.length == 0 && ETDAirList.value.length == 0) ||
       ETDFrequencyList.value.length == 0 ||
       ETDMethodsList.value == undefined ||
       ETDMethodsList.value.length == 0
@@ -513,21 +502,13 @@ const Savesubscribe = () => {
       missingmessage.value = missingmessage.value.substring(0, missingmessage.value.length - 2)
       UnableSaveVisible.value = true
     } else {
-      if(ETDOceanList.value.length == 0) {
-        str =
-        '[Air]' +
-        ETDAirList.value.join(',')
-      } else if(ETDAirList.value.length == 0) {
-        str =
-        '[Ocean]' +
-        ETDOceanList.value.join(',')
+      if (ETDOceanList.value.length == 0) {
+        str = '[Air]' + ETDAirList.value.join(',')
+      } else if (ETDAirList.value.length == 0) {
+        str = '[Ocean]' + ETDOceanList.value.join(',')
       } else {
         str =
-        '[Ocean]' +
-        ETDOceanList.value.join(',') +
-        ';\n[Air]' +
-        ETDAirList.value.join(',') +
-        ';'
+          '[Ocean]' + ETDOceanList.value.join(',') + ';\n[Air]' + ETDAirList.value.join(',') + ';'
       }
       savesubscribeobj.event_details = str
       SaveSuceessful()
@@ -578,14 +559,14 @@ const clearData = (val: any) => {
 const MilOceanref = ref()
 const MilAirref = ref()
 const ContainerOcean = ref()
-const handleCloseMilestoneOcean = (val:any) => {
+const handleCloseMilestoneOcean = (val: any) => {
   MilOceanref.value.hadleclose(val)
 }
-const handleCloseMilestoneAir = (val:any) => {
+const handleCloseMilestoneAir = (val: any) => {
   MilAirref.value.hadleclose(val)
 }
 
-const handleCloseContainer = (val:any) => {
+const handleCloseContainer = (val: any) => {
   ContainerOcean.value.hadleclose(val)
 }
 
@@ -936,12 +917,8 @@ defineExpose({
         </div>
       </template>
       <template #header>
-        <div class="cancel_header">
-          <span class="iconfont_icon iconfont_warning">
-            <svg class="iconfont icon_warning" aria-hidden="true">
-              <use xlink:href="#icon-icon_tipsfilled_b"></use>
-            </svg>
-          </span>
+        <div class="warning-header dialog-header">
+          <span class="font_family icon-icon_fail_fill_b"></span>
           Unsaved Changes
         </div>
       </template>
@@ -962,19 +939,15 @@ defineExpose({
         </div>
       </template>
       <template #header>
-        <div class="cancel_header">
-          <span class="iconfont_icon iconfont_warning">
-            <svg class="iconfont icon_danger" aria-hidden="true">
-              <use xlink:href="#icon-icon_fail_fill_b"></use>
-            </svg>
-          </span>
+        <div class="unable-save-header dialog-header">
+          <span class="font_family icon-icon_fail_fill_b"></span>
           Unable to Save
         </div>
       </template>
     </el-dialog>
     <!-- 保存成功 -->
     <el-dialog v-model="SaveedVisible" width="320" style="height: 212px">
-      <div style="text-align: center"><el-image :src="submitsucessful" style="width: 64px;" /></div>
+      <div style="text-align: center"><el-image :src="submitsucessful" style="width: 64px" /></div>
       <div style="text-align: center; margin-top: 20px">Saved successfully</div>
     </el-dialog>
   </div>
@@ -1086,12 +1059,23 @@ defineExpose({
   width: 100px;
   height: 40px;
 }
-.cancel_header {
-  font-size: 18px;
-  font-weight: 700;
-  color: var(--color-neutral-1);
+.dialog-header {
   display: flex;
   align-items: center;
+  .font_family {
+    font-size: 14px;
+    width: 16px;
+    height: 16px;
+    border-radius: 24px;
+    margin-right: 4px;
+  }
+}
+.unable-save-header .font_family {
+  color: #b53039;
+}
+
+div.warning-header .font_family {
+  color: #e9b227;
 }
 .icon_warning {
   width: 22px;
@@ -1112,4 +1096,4 @@ defineExpose({
 :deep(.el-collapse) {
   margin-right: 8px;
 }
-</style>
+</style>

+ 77 - 54
src/components/AutoSelect/src/AutoSelect.vue

@@ -1,27 +1,30 @@
 <script setup lang="ts">
-import { ref, watch } from 'vue'
+import { useFiltersStore } from '@/stores/modules/filtersList'
+import { cloneDeep, debounce } from 'lodash'
+import { useRoute } from 'vue-router'
+import axios from 'axios'
+
+const route = useRoute()
+const searchMode = route.path.includes('booking') ? 'booking' : 'tracking'
+const filtersStore = useFiltersStore()
 
 const props = defineProps({
-  ASPlaceholder: {
+  placeholder: {
     type: String
   },
-  ASValue: {
+  data: {
     type: Array
   },
-  ASisDisabled: {
-    type: Boolean
-  },
-  ASType: {
-    type: String
-  },
-  ASSearchFiled: {
+
+  type: {
     type: String
   },
-  ASSearchMode: {
+  title: {
     type: String
   },
-  ASSearchObj: {
-    type: Object
+  isDisabled: {
+    type: Boolean,
+    default: false
   }
 })
 
@@ -31,50 +34,56 @@ interface ListItem {
   checked: boolean
 }
 
-const list = ref<ListItem[]>([])
 const options = ref<ListItem[]>([])
-const value = ref(props.ASValue)
-const type = ref(props.ASType)
-const search_field = ref(props.ASSearchFiled)
-const search_mode = ref(props.ASSearchMode)
-const MoreFiltersObj = ref(props.ASSearchObj)
+const pageData = ref(cloneDeep(props.data))
 const loading = ref(false)
+const currentController = ref<AbortController | null>(null)
 watch(
-  () => props.ASValue,
-  (current) => {
-    value.value = current
-  }
-)
-watch(
-  () => props.ASSearchObj,
+  () => props.data,
   (current) => {
-    MoreFiltersObj.value = current
+    pageData.value = cloneDeep(current)
   }
 )
+
 const remoteMethod = (query: string) => {
   if (query) {
+    currentController.value?.abort()
+
+    const newController = new AbortController()
+    currentController.value = newController
     loading.value = true
-    setTimeout(() => {
-      $api
-        .getMoreFiltersData({
+
+    const queryData = filtersStore.getQueryData
+    $api
+      .getMoreFiltersData(
+        {
           term: query,
-          type: type.value,
-          search_field: search_field.value,
-          search_mode: search_mode.value,
-          ...MoreFiltersObj.value
-        })
-        .then((res: any) => {
+          type: props.type,
+          search_field: props.title,
+          search_mode: searchMode,
+          ...queryData
+        },
+        { signal: newController.signal }
+      )
+      .then((res: any) => {
+        if (!newController.signal.aborted && res.code == 200) {
+          options.value = res.data.map((item: any) => {
+            return { value: item, label: item, checked: pageData.value?.includes(item) }
+          })
+        }
+      })
+      .catch((err) => {
+        options.value = []
+        if (!axios.isCancel(err) && !newController.signal.aborted) {
+          ElMessage.error('Failed to load options')
+        }
+      })
+      .finally(() => {
+        // 仅当这是最新请求时,才关闭 loading
+        if (currentController.value === newController) {
           loading.value = false
-          if (res.code == 200) {
-            list.value = res.data.map((item: any) => {
-              return { value: item, label: item, checked: value.value?.includes(item) }
-            })
-            options.value = list.value.filter((item) => {
-              return item.label.toLowerCase().includes(query.toLowerCase())
-            })
-          }
-        })
-    }, 200)
+        }
+      })
   } else {
     options.value = []
   }
@@ -82,7 +91,7 @@ const remoteMethod = (query: string) => {
 const emit = defineEmits(['changeAutoSelect', 'changeFocus'])
 // 选中改变
 const changeAutoSelect = () => {
-  emit('changeAutoSelect', value)
+  emit('changeAutoSelect', pageData.value, props.title)
 }
 // 清除警告样式
 const removeClass = () => {
@@ -94,28 +103,42 @@ const removeClass = () => {
     }
   }
 }
+
+const handleBlur = () => {
+  emit('changeFocus', false)
+  nextTick(() => {
+    options.value = []
+  })
+}
+
+// 防抖版本(可选)
+const debouncedRemoteMethod = debounce(remoteMethod, 200)
+
+onUnmounted(() => {
+  currentController.value?.abort()
+})
 </script>
 <template>
   <div>
     <el-select
-      v-model="value"
+      v-model="pageData"
       multiple
       filterable
       remote
       @change="changeAutoSelect"
       reserve-keyword
-      :placeholder="props.ASPlaceholder"
+      :placeholder="props.placeholder"
       collapse-tags
       @focus="removeClass"
-      @blur="emit('changeFocus', false)"
-      :disabled="props.ASisDisabled"
+      @blur="handleBlur"
+      :disabled="props.isDisabled"
       collapse-tags-tooltip
       :max-collapse-tags="3"
-      :remote-method="remoteMethod"
+      :remote-method="debouncedRemoteMethod"
       :loading="loading"
     >
       <el-option v-for="item in options" :key="item.value" :label="item.label" :value="item.value">
-        <el-checkbox :checked="item.checked">
+        <el-checkbox :checked="item.checked" style="flex: 1">
           <span class="label" @click="item.checked = !item.checked">{{ item.value }}</span>
         </el-checkbox>
       </el-option>
@@ -142,4 +165,4 @@ const removeClass = () => {
     color: var(--badge__content--warning);
   }
 }
-</style>
+</style>

+ 21 - 22
src/components/CreateAddRules/src/CreateAddRules.vue

@@ -1240,12 +1240,8 @@ defineExpose({
       </div>
     </template>
     <template #header>
-      <div class="cancel_header">
-        <span class="iconfont_icon iconfont_warning">
-          <svg class="iconfont icon_danger" aria-hidden="true">
-            <use xlink:href="#icon-icon_fail_fill_b"></use>
-          </svg>
-        </span>
+      <div class="unable-save-header dialog-header">
+        <span class="font_family icon-icon_fail_fill_b"></span>
         Unable to Save
       </div>
     </template>
@@ -1267,12 +1263,8 @@ defineExpose({
       </div>
     </template>
     <template #header>
-      <div class="cancel_header">
-        <span class="iconfont_icon iconfont_warning">
-          <svg class="iconfont icon_danger" aria-hidden="true">
-            <use xlink:href="#icon-icon_fail_fill_b"></use>
-          </svg>
-        </span>
+      <div class="unable-save-header dialog-header">
+        <span class="font_family icon-icon_fail_fill_b"></span>
         Unable to Save
       </div>
     </template>
@@ -1300,12 +1292,8 @@ defineExpose({
       </div>
     </template>
     <template #header>
-      <div class="cancel_header">
-        <span class="iconfont_icon iconfont_warning">
-          <svg class="iconfont icon_warning" aria-hidden="true">
-            <use xlink:href="#icon-icon_tipsfilled_b"></use>
-          </svg>
-        </span>
+      <div class="warning-header dialog-header">
+        <span class="font_family icon-icon_warning_fill_b"></span>
         Similar Rule Detected
       </div>
     </template>
@@ -1415,12 +1403,23 @@ defineExpose({
   width: 100px;
   height: 40px;
 }
-.cancel_header {
-  font-size: 18px;
-  font-weight: 700;
-  color: var(--color-neutral-1);
+.dialog-header {
   display: flex;
   align-items: center;
+  .font_family {
+    font-size: 14px;
+    width: 16px;
+    height: 16px;
+    border-radius: 24px;
+    margin-right: 4px;
+  }
+}
+.unable-save-header .font_family {
+  color: #b53039;
+}
+
+div.warning-header .font_family {
+  color: #e9b227;
 }
 .icon_warning {
   width: 22px;

+ 3 - 0
src/components/CustomizeColumns/src/CustomizeColumns.vue

@@ -102,6 +102,9 @@ const scrollToItem = (itemId: string) => {
       // container.scrollTop = targetElement.offsetTop - container.offsetTop
 
       document.addEventListener('click', handleDocumentClick)
+      setTimeout(() => {
+        searchColumn.value = ''
+      }, 600)
     }
   }, 100)
 }

+ 115 - 248
src/components/DateRange/src/DateRange.vue

@@ -1,254 +1,126 @@
 <script setup lang="ts">
-import emitter from '@/utils/bus'
 import { ref, watch, onMounted, onBeforeMount } from 'vue'
 import IconDropDown from '@/components/IconDropDown'
-import CalendarDate from './components/CalendarDate.vue'
-import dayjs from 'dayjs'
-import { useUserStore } from '@/stores/modules/user'
+import VCalendarDate from './components/VCalendarDate.vue'
+import { useFiltersStore } from '@/stores/modules/filtersList'
+import { useRoute } from 'vue-router'
 
-const userStore = useUserStore()
-const formatDate = userStore.dateFormat
-const valueFormatDate = 'MM/DD/YYYY'
+const route = useRoute()
+const searchMode = route.path.includes('booking') ? 'booking' : 'tracking'
+const etaKey = searchMode === 'booking' ? ['f_eta_start', 'f_eta_end'] : ['eta_start', 'eta_end']
+const etdKey = searchMode === 'booking' ? ['f_etd_start', 'f_etd_end'] : ['etd_start', 'etd_end']
 
-onMounted(() => {
-  const emitPayload = defaultDate()
-  // 统一 emit(只调用一次)
-  emit('defaultDate', daterangeObj2, emitPayload)
-  emitter.on('clearTag', (tag: any) => {
-    if (tag.includes('ETD')) {
-      clearETdDateRange()
-    }
-    if (tag.includes('ETA')) {
-      clearEtaDateRange()
-    }
-    if (tag.includes('Creation Time')) {
-      clearEtaDateRange()
-    }
-  })
-  emitter.on('clearDaterangeObj', () => {
-    clearDaterangeObj()
-  })
-})
-
-onBeforeMount(() => {
-  emitter.off('clearTag')
-  emitter.off('clearDaterangeObj')
-})
+const filtersStore = useFiltersStore()
+const filtersList = computed(() => filtersStore.filtersList)
 
-const Date_visible = ref(false)
-const DateType = ref()
-const DateTypeoptions = ref([
+const popoverVisible = ref(false)
+const allOtherType = ref([
   {
-    value: 'Creation Time',
-    label: 'Creation Time'
+    title: 'Creation Time',
+    key: ['created_time_start', 'created_time_end']
   }
 ])
-const AddDateType = ref()
-AddDateType.value = []
-const AddType = () => {
-  AddDateType.value.push({
+const otherDateType = ref([])
+
+const getRangeDate = (title: string) => {
+  const data = filtersStore.getFilterByTitle(title)
+  if (data) {
+    const [startStr, endStr] = data.value
+    return [startStr, endStr]
+  } else {
+    return ['', '']
+  }
+}
+const initDate = () => {
+  etdDateRange.value = getRangeDate('ETD')
+  etaDateRange.value = getRangeDate('ETA')
+
+  otherDateType.value = []
+  filtersList.value.forEach((item) => {
+    const curIndex = allOtherType.value.findIndex((type) => type.title === item.title)
+    if (curIndex !== -1) {
+      otherDateType.value.push({
+        title: item.title,
+        value: getRangeDate(item.title)
+      })
+    }
+  })
+}
+
+const addType = () => {
+  otherDateType.value.push({
     label: '',
-    value: ''
+    value: ['', '']
   })
 }
 const deleteType = (i: any) => {
-  AddDateType.value.splice(i, 1)
-  DateType.value = ''
-  clearDateCreation()
-}
-const props = defineProps({
-  isShipment: Boolean
-})
-const etdDateRange = ref()
-etdDateRange.value = []
-const etaDateRange = ref()
-etaDateRange.value = []
-const DateCreation = ref()
-// 查询默认日期
-const defaultDate = () => {
-  // 工具函数:安全格式化日期范围
-  const formatDateRange = (startStr: string | null, endStr: string | null) => {
-    if (!startStr || !endStr) return [null, null]
-    const start = dayjs(startStr)
-    const end = dayjs(endStr)
-    return start.isValid() && end.isValid()
-      ? [start.format(formatDate), end.format(formatDate)]
-      : [null, null]
-  }
+  otherDateType.value.splice(i, 1)
+}
 
-  // 工具函数:设置 daterangeObj2 条目
-  const setRange = (key: string, title: string, startStr: string | null, endStr: string | null) => {
-    const [start, end] = formatDateRange(startStr, endStr)
-    if (start && end) {
-      daterangeObj2[key] = { title, data: [start, end] }
-    }
-  }
+const etdDateRange = ref([])
+const etaDateRange = ref([])
 
-  // 工具函数:设置 AddDateType(仅用于 Creation Time)
-  const setAddDateType = (startStr: string | null, endStr: string | null) => {
-    const [start, end] = formatDateRange(startStr, endStr)
-    AddDateType.value = [{ label: 'Creation Time', value: [start, end] }]
+const emit = defineEmits(['DateRangeSearch', 'clearDaterangeTags'])
+const hasValidDate = (date: any) => {
+  return date && date.length === 2 && date[0] && date[1]
+}
+const DateRangeSearch = () => {
+  if (hasValidDate(etaDateRange.value)) {
+    filtersStore.updateFilter({
+      title: 'ETA',
+      keyType: 'dateRange',
+      value: etaDateRange.value,
+      key: etaKey
+    })
+  } else {
+    filtersStore.deleteFilterByTitle('ETA')
   }
 
-  // 默认时间范围(2个月前月初 到 下个月)
-  const getDefaultRange = () => [
-    dayjs().subtract(2, 'month').startOf('month').format(formatDate),
-    dayjs().add(1, 'month').format(formatDate)
-  ]
-
-  // ----------------------------
-  // 主逻辑开始
-  // ----------------------------
-
-  let trackingData: Record<string, any> = {}
-  let emitPayload: Record<string, any> = {}
-
-  if (props.isShipment) {
-    const clickParams = sessionStorage.getItem('clickParams')
-    if (clickParams && clickParams !== '{}') {
-      // 场景 A: 有 clickParams → 读取 reportList
-      const reportList = JSON.parse(sessionStorage.getItem('reportList') || '{}')
-      trackingData = JSON.parse(sessionStorage.getItem('searchTableQeuryTracking') || '{}')
-      etdDateRange.value = [
-        reportList.etd_start
-          ? dayjs(reportList.etd_start, valueFormatDate).format(formatDate)
-          : null,
-        reportList.etd_end ? dayjs(reportList.etd_end, valueFormatDate).format(formatDate) : null
-      ]
-      etaDateRange.value = [
-        reportList.eta_start
-          ? dayjs(reportList.eta_start, valueFormatDate).format(formatDate)
-          : null,
-        reportList.eta_end ? dayjs(reportList.eta_end, valueFormatDate).format(formatDate) : null
-      ]
-      setRange('ETD', 'ETD', reportList.etd_start, reportList.etd_end)
-      setRange('ETA', 'ETA', reportList.eta_start, reportList.eta_end)
-    } else {
-      // 场景 B: 无 clickParams → 读取 searchTableQeuryTracking
-      const stored = sessionStorage.getItem('searchTableQeuryTracking')
-      if (!stored) {
-        // 子场景 B1: 无存储 → 用默认值
-        const [start, end] = getDefaultRange()
-        etdDateRange.value = [start, end]
-        setRange('ETD', 'ETD', start, end)
-      } else {
-        // 子场景 B2: 有存储
-        trackingData = JSON.parse(stored)
-        etdDateRange.value = [
-          trackingData.etd_start ? trackingData.etd_start : null,
-          trackingData.etd_end ? trackingData.etd_end : null
-        ]
-        etaDateRange.value = [
-          trackingData.eta_start ? trackingData.eta_start : null,
-          trackingData.eta_end ? trackingData.eta_end : null
-        ]
-        setRange('ETD', 'ETD', trackingData.etd_start, trackingData.etd_end)
-        setRange('ETA', 'ETA', trackingData.eta_start, trackingData.eta_end)
-        if (trackingData.created_time_start) {
-          setAddDateType(trackingData.created_time_start, trackingData.created_time_end)
-          setRange(
-            'Creation Time',
-            'Creation Time',
-            trackingData.created_time_start,
-            trackingData.created_time_end
-          )
-        }
-      }
-    }
-    emitPayload = trackingData
+  if (hasValidDate(etdDateRange.value)) {
+    filtersStore.updateFilter({
+      title: 'ETD',
+      keyType: 'dateRange',
+      value: etdDateRange.value,
+      key: etdKey
+    })
   } else {
-    // 非 shipment 场景
-    const stored = sessionStorage.getItem('searchTableQeury')
-    if (!stored) {
-      // 无存储 → 默认值
-      const [start, end] = getDefaultRange()
-      etdDateRange.value = [start, end]
-      setRange('ETD', 'ETD', start, end)
-    } else {
-      // 有存储
-      const queryData = JSON.parse(stored)
-      emitPayload = queryData
-
-      etdDateRange.value = [
-        queryData.f_etd_start ? queryData.f_etd_start : null,
-        queryData.f_etd_end ? queryData.f_etd_end : null
-      ]
-      etaDateRange.value = [
-        queryData.m_eta_start ? queryData.m_eta_start : null,
-        queryData.m_eta_end ? queryData.m_eta_end : null
-      ]
-      setRange('ETD', 'ETD', queryData.f_etd_start, queryData.f_etd_end)
-      setRange('ETA', 'ETA', queryData.m_eta_start, queryData.m_eta_end)
-      if (queryData.created_time_start) {
-        setAddDateType(queryData.created_time_start, queryData.created_time_end)
-        setRange(
-          'Creation Time',
-          'Creation Time',
-          queryData.created_time_start,
-          queryData.created_time_end
-        )
-      }
-    }
+    filtersStore.deleteFilterByTitle('ETD')
   }
-  return emitPayload
-}
-const daterangedata = ref()
-daterangedata.value = []
-let daterangeObj2: any = {}
-const DateRangeChange = (val: any) => {
-  if (val.data != null) {
-    val.data = [
-      dayjs(val.data[0], valueFormatDate).format(formatDate),
-      dayjs(val.data[1], valueFormatDate).format(formatDate)
-    ]
-    daterangeObj2[val.title] = val
-  } else {
-    delete daterangeObj2[val.title]
-    if (val.title == 'ETD') {
-      etdDateRange.value = []
-    } else if (val.title == 'ETA') {
-      etaDateRange.value = []
+
+  allOtherType.value.forEach((item) => {
+    const curIndex = otherDateType.value.findIndex((type) => type.title === item.title)
+    if (curIndex !== -1 && hasValidDate(otherDateType.value[curIndex]?.value)) {
+      filtersStore.updateFilter({
+        title: item.title,
+        keyType: 'dateRange',
+        value: otherDateType.value[curIndex].value,
+        key: item.key
+      })
     } else {
-      DateCreation.value = []
-      AddDateType.value = []
-      DateType.value = ''
+      filtersStore.deleteFilterByTitle(item.title)
     }
-  }
-}
-const emit = defineEmits(['DateRangeSearch', 'clearDaterangeTags', 'defaultDate'])
-const DateRangeSearch = () => {
-  emit('DateRangeSearch', daterangeObj2)
-  Date_visible.value = false
+  })
+  emit('DateRangeSearch')
+  popoverVisible.value = false
 }
 
 // 清除
 const clearRest = () => {
+  etdDateRange.value = []
+  etaDateRange.value = []
+  otherDateType.value = []
+  filtersStore.deleteFilterByTitle('ETA')
+  filtersStore.deleteFilterByTitle('ETD')
+  allOtherType.value.forEach((item) => {
+    filtersStore.deleteFilterByTitle(item.title)
+  })
   emit('clearDaterangeTags')
-  clearETdDateRange()
-  clearEtaDateRange()
-  clearDateCreation()
-  clearDaterangeObj()
 }
-// 清除EDT
-const clearETdDateRange = () => {
+
+const closeRset = () => {
   etdDateRange.value = []
-  delete daterangeObj2['ETD']
-}
-// 清除EDA
-const clearEtaDateRange = () => {
   etaDateRange.value = []
-  delete daterangeObj2['ETA']
-}
-// 清除Creation Time
-const clearDateCreation = () => {
-  DateCreation.value = []
-  AddDateType.value = []
-  DateType.value = ''
-  delete daterangeObj2['Creation Time']
-}
-// 清除 daterangeObj
-const clearDaterangeObj = () => {
-  daterangeObj2 = {}
+  otherDateType.value = []
 }
 </script>
 <template>
@@ -256,13 +128,17 @@ const clearDaterangeObj = () => {
     <el-popover
       trigger="click"
       :width="400"
-      :visible="Date_visible"
+      :visible="popoverVisible"
       popper-class="DaterangeClass"
-      @before-enter="defaultDate()"
-      @hide="clearDateCreation()"
+      @before-enter="initDate()"
+      @close="closeRset"
     >
       <template #reference>
-        <div class="Date_Range" @blur="Date_visible = false" @click="Date_visible = !Date_visible">
+        <div
+          class="Date_Range"
+          @blur="popoverVisible = false"
+          @click="popoverVisible = !popoverVisible"
+        >
           <div class="select_title">Date Range</div>
           <span class="iconfont_icon">
             <svg class="iconfont icon_dark" aria-hidden="true">
@@ -274,20 +150,12 @@ const clearDaterangeObj = () => {
       <div class="date_header">
         <div class="title">Date Range</div>
         <div class="ETD">
-          <CalendarDate
-            CalendarTitle="ETD"
-            :Date="etdDateRange"
-            @DateRangeChange="DateRangeChange"
-          ></CalendarDate>
+          <VCalendarDate CalendarTitle="ETD" v-model:date="etdDateRange"></VCalendarDate>
         </div>
         <div class="ETA">
-          <CalendarDate
-            CalendarTitle="ETA"
-            :Date="etaDateRange"
-            @DateRangeChange="DateRangeChange"
-          ></CalendarDate>
+          <VCalendarDate CalendarTitle="ETA" v-model:date="etaDateRange"></VCalendarDate>
         </div>
-        <div class="AddType" v-for="(item, index) in AddDateType" :key="item.value">
+        <div class="addType" v-for="(item, index) in otherDateType" :key="item.title">
           <div>
             <div class="ETD_title Date_Title">
               <div class="Date_type">Date Type</div>
@@ -300,28 +168,27 @@ const clearDaterangeObj = () => {
             <el-select
               :suffix-icon="IconDropDown"
               placeholder="Please Select Date Type"
-              v-model="item.label"
+              v-model="item.title"
             >
               <el-option
-                v-for="item in DateTypeoptions"
-                :key="item.value"
-                :label="item.label"
-                :value="item.value"
+                v-for="ite in allOtherType"
+                :key="ite.title"
+                :label="ite.title"
+                :value="ite.title"
               >
               </el-option>
             </el-select>
           </div>
           <div style="margin-top: 16px">
-            <CalendarDate
+            <VCalendarDate
               :CalendarTitle="item.label || 'Date Range'"
               CalendarWidth="352px"
-              :Date="item.value"
-              @DateRangeChange="DateRangeChange"
+              v-model:date="item.value"
               :isType="true"
-            ></CalendarDate>
+            ></VCalendarDate>
           </div>
         </div>
-        <div class="MoreType" @click="AddType" v-if="AddDateType.length != DateTypeoptions.length">
+        <div class="MoreType" @click="addType" v-if="otherDateType.length != allOtherType.length">
           <el-button class="el-button--noborder moretype">+ More Date Type</el-button>
         </div>
         <div class="daterange_bottom">
@@ -416,7 +283,7 @@ const clearDaterangeObj = () => {
   border-radius: var(--border-radius-6);
   height: 40px;
 }
-.AddType {
+.addType {
   background-color: var(--color-header-bg);
   margin: 0 16px 8px 16px;
   padding: 8px;

+ 3 - 0
src/components/DateRange/src/components/QuickCalendarDate.vue

@@ -26,6 +26,9 @@ watch(
   () => props.Date,
   (current) => {
     ETDDate.value = current
+  },
+  {
+    immediate: true
   }
 )
 const emit = defineEmits(['DateRangeChange', 'DateChange'])

+ 172 - 0
src/components/DateRange/src/components/VCalendarDate.vue

@@ -0,0 +1,172 @@
+<script lang="ts" setup>
+import dayjs, { Dayjs } from 'dayjs'
+import { ref, watch } from 'vue'
+import { useUserStore } from '@/stores/modules/user'
+
+const userStore = useUserStore()
+const formatDate = userStore.dateFormat
+const valueFormatDate = 'MM/DD/YYYY'
+const props = defineProps({
+  CalendarWidth: {
+    type: String,
+    default: '368px'
+  },
+  CalendarTitle: {
+    type: String
+  },
+  date: {
+    type: Array
+  },
+  isType: {
+    type: Boolean,
+    default: false
+  },
+  isNeedFooter: {
+    type: Boolean,
+    default: true
+  },
+  isETA: {
+    type: Boolean,
+    default: false
+  },
+  isShowPopupClass: {
+    type: Boolean,
+    default: false
+  }
+})
+const ETDDate = ref([])
+const CreatePlaceholder = computed(() => {
+  if (props.isETA) {
+    return ['Start ETA Time', 'End ETA Time']
+  } else {
+    return ['Start ATA Time', 'End ATA Time']
+  }
+})
+
+const emit = defineEmits(['DateRangeChange', 'DateChange', 'update:date'])
+const open = ref(false)
+const isShowExtra = ref(true)
+
+const DateList = ref()
+DateList.value = []
+const daterange = (val: any) => {
+  if (DateList.value.length == 2 && val != undefined) {
+    const rangedata = {
+      title: props.CalendarTitle,
+      data: DateList.value
+    }
+    emit('DateRangeChange', rangedata)
+  }
+  emit('update:date', props.date)
+}
+const ChangeToday = (val: any) => {
+  if (val == 'Earliest') {
+    DateList.value[0] = dayjs().format(valueFormatDate)
+    daterange(DateList.value[1])
+  } else {
+    DateList.value[1] = dayjs().format(valueFormatDate)
+    daterange(DateList.value[0])
+  }
+}
+
+const Earliest = () => {
+  DateList.value[0] = dayjs('Oct-05-2009').format(valueFormatDate)
+  daterange(DateList.value[1])
+}
+const Latest = () => {
+  DateList.value[1] = dayjs().format(valueFormatDate)
+  daterange(DateList.value[0])
+}
+const changeRangeData = (value: any) => {
+  emit('update:date', value)
+}
+const handleCalendarOpen = (date: any) => {
+  open.value = !open.value
+  if (open.value == false) {
+    if (date.length != 2) {
+      DateList.value = []
+      ETDDate.value = []
+    }
+  }
+}
+</script>
+<template>
+  <div>
+    <div class="ETD_title" v-if="props.CalendarTitle">{{ props.CalendarTitle }}</div>
+    <a-range-picker
+      separator="To"
+      :showToday="false"
+      :style="{
+        width: props.CalendarWidth,
+        backgroundColor: props.isType ? 'var(--more-type-bg-color)' : 'var(--management-bg-color)'
+      }"
+      :popupClassName="props.isShowPopupClass ? 'th-color' : ''"
+      :open="open"
+      @change="changeRangeData"
+      :placeholder="isNeedFooter ? ['Start Time', 'End Time'] : CreatePlaceholder"
+      :format="formatDate"
+      @openChange="handleCalendarOpen(ETDDate)"
+      :valueFormat="formatDate"
+      :value="props.date"
+    >
+      <template #suffixIcon>
+        <span class="iconfont_icon">
+          <svg class="iconfont icon_suffix" aria-hidden="true">
+            <use xlink:href="#icon-icon_date_b"></use>
+          </svg>
+        </span>
+      </template>
+      <template #renderExtraFooter v-if="isShowExtra && isNeedFooter">
+        <div class="calender_flex">
+          <div class="footer_left">
+            <el-button class="el-button--noborder" @click="Earliest">Earliest Time</el-button>
+            <el-button class="el-button--noborder" @click="ChangeToday('Earliest')"
+              >Today</el-button
+            >
+          </div>
+          <div class="footer_left footer_right">
+            <el-button @click="Latest" class="el-button--noborder">Latest Time</el-button>
+            <el-button class="el-button--noborder" @click="ChangeToday('Latest')">Today</el-button>
+          </div>
+        </div>
+      </template>
+    </a-range-picker>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.calender_flex {
+  display: flex;
+  justify-content: space-between;
+}
+.footer_left {
+  display: flex;
+  flex: 50%;
+  padding: 0 0 0 5px;
+  height: 48px;
+  display: flex;
+  align-items: center;
+}
+.footer_right {
+  border-left: 1px solid var(--border-color-2);
+  padding-left: 8px;
+}
+.el-button--noborder {
+  color: var(--color-theme);
+}
+.ETD_title {
+  font-size: var(--font-size-2);
+  margin-bottom: 4px;
+  color: var(--color-neutral-2);
+}
+.iconfont_icon {
+  margin-right: 0;
+}
+.iconfont {
+  width: 14px;
+  height: 14px;
+}
+.icon_suffix {
+  fill: var(--color-neutral-1);
+}
+</style>

+ 51 - 66
src/components/FliterTags/src/FilterTags.vue

@@ -1,6 +1,6 @@
 <script setup lang="ts">
-import { ref, onMounted, onBeforeMount, watch, computed } from 'vue'
-import emitter from '@/utils/bus'
+import { cloneDeep } from 'lodash'
+
 interface ListItem {
   name: string
   number: number
@@ -8,73 +8,58 @@ interface ListItem {
   checked: boolean
 }
 interface Props {
-  TagsListItem: ListItem[]
+  tagsList: ListItem[]
 }
 const props = withDefaults(defineProps<Props>(), {})
 
-const TagsList = ref(props.TagsListItem)
-const NewTagsList = ref()
-
-watch(
-  () => props.TagsListItem,
-  (current) => {
-    TagsList.value = current
-  }
-)
-onMounted(() => {
-  emitter.on('clearTag', (tag: any) => {
-    if (tag.includes('Shipment status')) {
-      checkedCount = []
-      // TagsList.value.forEach((item: any) => {
-      //   item.checked = false
-      // })
-      // TagsList.value[0].checked = true
-      NewTagsList.value = ['All']
-      emits('changeTag', NewTagsList.value, false)
-    }
-  })
-})
+const emits = defineEmits(['tabChange'])
 
-onBeforeMount(() => {
-  emitter.off('clearTag')
-})
-
-NewTagsList.value = []
-
-let checkedCount: any[] = []
-const emits = defineEmits(['changeTag'])
-const checkedBox = (i: any, name: any, checked: any) => {
-  if (name == 'All') {
-    checkedCount = []
-    TagsList.value.forEach((item: any) => {
-      item.checked = false
-    })
-    TagsList.value[0].checked = true
-  } else if (!checked) {
-    TagsList.value[0].checked = false
-    TagsList.value[i].checked = !TagsList.value[i].checked
-    checkedCount.push(name)
-    const map = new Map()
-    checkedCount.forEach((item) => map.set(item, true))
-    checkedCount = [...map.keys()]
+const getCheckedTabs = (tagsList: ListItem[]) => {
+  const checkedList = tagsList.filter((item) => item.checked)
+  return checkedList.map((item) => item.name)
+}
+// 判断是否除了all,其他的全选了
+const isAllExceptAllSelected = (tagsList: ListItem[]) => {
+  const curCheckedTags = getCheckedTabs(tagsList)
+  if (curCheckedTags.length === tagsList.length - 1 && !curCheckedTags.includes('All')) {
+    return true
   } else {
-    checkedCount.splice(checkedCount.indexOf(name), 1)
-    TagsList.value[0].checked = false
-    TagsList.value[i].checked = !TagsList.value[i].checked
+    return false
   }
-  if (checkedCount.length === TagsList.value.length - 1 || checkedCount.length == 0) {
-    checkedCount = []
-    TagsList.value.forEach((item: any) => {
-      item.checked = false
+}
+//  点击标签 如果除了all
+const handleTagToggle = (index: any, checked: any) => {
+  // 如果点击的是all,并且当前选中也是all,那么直接返回
+  if (index === 0 && checked) return
+
+  const curTagList = cloneDeep(props.tagsList)
+  if (index !== 0) {
+    const curTag = curTagList[index]
+    const curCheckedTags = getCheckedTabs(curTagList)
+    // 如果只选中了一个标签,并且这个标签不是all,那么改为选中all,取消选中当前选项
+    if (curCheckedTags.includes(curTag.name) && curCheckedTags.length === 1) {
+      curTagList[index].checked = false
+      curTagList[0].checked = true
+      emits('tabChange', curTagList)
+      return
+    }
+    curTagList[index].checked = !checked
+    const isCheckedAll = isAllExceptAllSelected(curTagList)
+    if (isCheckedAll) {
+      curTagList[0].checked = true
+      curTagList.forEach((item, index) => {
+        index !== 0 && (item.checked = false)
+      })
+    } else {
+      curTagList[0].checked = false
+    }
+  } else if (index === 0 && !checked) {
+    curTagList[0].checked = true
+    curTagList.forEach((item, index) => {
+      index !== 0 && (item.checked = false)
     })
-    TagsList.value[0].checked = true
-  }
-  if (checkedCount.length == 0) {
-    NewTagsList.value = ['All']
-  } else {
-    NewTagsList.value = checkedCount
   }
-  emits('changeTag', NewTagsList.value, true)
+  emits('tabChange', curTagList)
 }
 </script>
 
@@ -83,10 +68,10 @@ const checkedBox = (i: any, name: any, checked: any) => {
     <div class="TagsBox">
       <div
         class="list"
-        v-for="(item, index) of TagsList"
-        :key="index"
+        v-for="(item, index) of props.tagsList"
+        :key="item.name"
         :class="[item.checked ? 'checked' : '']"
-        @click="checkedBox(index, item.name, item.checked)"
+        @click="handleTagToggle(index, item.checked)"
       >
         {{ item.name }}
         <div
@@ -150,7 +135,7 @@ const checkedBox = (i: any, name: any, checked: any) => {
   background-color: var(--color-tag-cancelled-bg);
   color: var(--color-tag-cancelled);
 }
-.v-tag__departure {
+.v-tag__departed {
   font-weight: 400;
   background-color: var(--color-tag-departure-bg);
   color: var(--color-tag-departure);
@@ -179,4 +164,4 @@ const checkedBox = (i: any, name: any, checked: any) => {
   background-color: var(--color-tag-all-bg);
   color: var(--color-tag-all);
 }
-</style>
+</style>

File diff ditekan karena terlalu besar
+ 151 - 975
src/components/MoreFilters/src/MoreFilters.vue


+ 237 - 0
src/components/MoreFilters/src/components/PartiesView.vue

@@ -0,0 +1,237 @@
+<script setup lang="ts">
+import { useFiltersStore } from '@/stores/modules/filtersList'
+import { useRoute } from 'vue-router'
+
+const route = useRoute()
+const searchMode = route.path.includes('booking') ? 'booking' : 'tracking'
+
+const filtersStore = useFiltersStore()
+const filtersList = computed(() => filtersStore.filtersList)
+
+const pageData = ref([
+  {
+    title: 'Shipper Name',
+    key: 'shipper',
+    type: 'contanct',
+    value: [],
+    placeholder: 'Please input shipper name'
+  },
+  {
+    title: 'Consignee Name',
+    key: 'consignee',
+    type: 'contanct',
+    value: [],
+    placeholder: 'Please input consignee name'
+  }
+])
+const changeAutoSelect = (data: any, title: string) => {
+  const index = pageData.value.findIndex((item: any) => item.title === title)
+  pageData.value[index].value = data
+}
+
+const partyTypeOptions = ref([
+  {
+    title: 'Origin Agent',
+    key: 'origin',
+    type: 'apex'
+  },
+  {
+    title: 'Destination Agent',
+    key: 'agent',
+    type: 'apex'
+  },
+  {
+    title: 'Sales',
+    key: 'sales_rep',
+    type: 'sales'
+  }
+])
+if (searchMode === 'tracking') {
+  partyTypeOptions.value.push(
+    {
+      title: 'Notify Party',
+      key: 'notify',
+      type: 'apex'
+    },
+    {
+      title: 'Bill to',
+      key: 'bill_to',
+      type: 'apex'
+    },
+    {
+      title: 'Destination Operator',
+      key: 'dest_op',
+      type: 'sales'
+    }
+  )
+}
+
+const moreList = ref([])
+
+const initData = () => {
+  moreList.value = []
+  pageData.value.forEach((item) => {
+    item.value = []
+  })
+  filtersList.value.forEach((item) => {
+    pageData.value.forEach((pageItem) => {
+      if (item.title === pageItem.title) {
+        pageItem.value = item.value as string[]
+      }
+    })
+    partyTypeOptions.value.forEach((typeItem) => {
+      if (item.title === typeItem.title) {
+        moreList.value.push({
+          title: item.title,
+          value: item.value
+        })
+      }
+    })
+  })
+}
+
+const generate3UniqueIds = () => {
+  return Math.floor(Math.random() * 900000) + 100000
+}
+const addType = () => {
+  moreList.value.push({
+    title: '',
+    id: generate3UniqueIds(),
+    value: []
+  })
+}
+
+const changeTitle = (data: any, title: string) => {
+  const index = moreList.value.findIndex((item) => item.id === data.id)
+  moreList.value[index].title = title
+  moreList.value[index].value = []
+}
+const changeValue = (data: any, value: any) => {
+  const index = moreList.value.findIndex((item) => item.id === data.id)
+  moreList.value[index].value = value
+}
+const deleteItem = (title: string) => {
+  const index = moreList.value.findIndex((item) => item.title === title)
+  moreList.value.splice(index, 1)
+}
+
+const getQueryData = () => {
+  pageData.value.forEach((item: any) => {
+    if (item.value.length) {
+      filtersStore.updateFilter({
+        title: item.title,
+        key: item.key,
+        value: item.value,
+        keyType: 'array'
+      })
+    } else {
+      filtersStore.deleteFilterByTitle(item.title)
+    }
+  })
+  moreList.value.forEach((item) => {
+    partyTypeOptions.value.forEach((ite) => {
+      if (item.title === ite.title) {
+        item.key = ite.key
+      }
+    })
+  })
+
+  partyTypeOptions.value.forEach((item) => {
+    const index = moreList.value.findIndex((ite) => ite.title === item.title && ite.value.length)
+    if (index !== -1) {
+      const ite = moreList.value[index]
+      filtersStore.updateFilter({
+        title: ite.title,
+        key: ite.key,
+        value: ite.value,
+        keyType: 'array'
+      })
+    } else {
+      filtersStore.deleteFilterByTitle(item.title)
+    }
+  })
+}
+const getBadgeData = () => {
+  let count = 0
+  pageData.value.forEach((item) => {
+    if (item.value.length) {
+      count++
+    }
+  })
+  moreList.value.forEach((item) => {
+    if (item.value.length) {
+      count++
+    }
+  })
+  return count
+}
+
+const resetData = () => {
+  pageData.value.forEach((item) => {
+    item.value = []
+  })
+}
+
+defineExpose({
+  getQueryData,
+  initData,
+  getBadgeData,
+  resetData
+})
+</script>
+
+<template>
+  <div>
+    <div class="ETD" v-for="item in pageData" :key="item.title">
+      <div class="ETD_title">{{ item.title }}</div>
+      <AutoSelect
+        :type="item.type"
+        :title="item.title"
+        :data="item.value"
+        :placeholder="item.placeholder"
+        @changeAutoSelect="changeAutoSelect"
+      >
+      </AutoSelect>
+    </div>
+
+    <SelectAutoSelect
+      :typeOptions="partyTypeOptions"
+      :data="moreList"
+      @deleteItem="deleteItem"
+      @changeTitle="changeTitle"
+      @changeValue="changeValue"
+      placeholder="Please input party name"
+    >
+    </SelectAutoSelect>
+    <div class="MoreType" @click="addType" v-if="partyTypeOptions.length != moreList.length">
+      <el-button class="el-button--noborder moretype">+ More Party Type</el-button>
+    </div>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.ETD {
+  margin: 8px 16px;
+}
+
+.ETA {
+  margin: 16px 16px;
+}
+
+.ETD_title {
+  font-size: var(--font-size-2);
+  color: var(--color-neutral-2);
+}
+
+.MoreType {
+  color: var(--color-accent-2);
+  padding: 0 0 16px 16px;
+  cursor: pointer;
+}
+
+.moretype {
+  background-color: transparent;
+  padding: 0 4px;
+  height: 24px;
+}
+</style>

+ 230 - 0
src/components/MoreFilters/src/components/PlacesView.vue

@@ -0,0 +1,230 @@
+<script setup lang="ts">
+import { useFiltersStore } from '@/stores/modules/filtersList'
+import { cloneDeep } from 'lodash'
+import { useRoute } from 'vue-router'
+
+const route = useRoute()
+const searchMode = route.path.includes('booking') ? 'booking' : 'tracking'
+
+const filtersStore = useFiltersStore()
+const filtersList = computed(() => filtersStore.filtersList)
+
+const pageData = ref([
+  {
+    title: 'Origin',
+    key: 'shipper_city',
+    value: [],
+    placeholder: 'Please input shipper name'
+  },
+  {
+    title: 'Destination',
+    key: 'consignee_city',
+    value: [],
+    placeholder: 'Please input consignee name'
+  }
+])
+const moreList = ref([])
+const placeTypeOptions = ref([
+  {
+    title: 'Place of delivery',
+    key: 'place_of_delivery/place_of_delivery_exp',
+    needKey: 'city'
+  },
+  {
+    title: ' Place of Receipt',
+    key: 'place_of_receipt/place_of_receipt_exp',
+    needKey: 'city'
+  },
+  {
+    title: 'Port of Loading',
+    key: 'fport_of_loading_uncode/fport_of_loading_exp',
+    type: 'uncode'
+  }
+])
+if (searchMode === 'tracking') {
+  placeTypeOptions.value.push({
+    title: 'Place of Discharge',
+    key: 'Place of Discharge',
+    needKey: 'city'
+  })
+}
+
+const initData = () => {
+  moreList.value = []
+  pageData.value.forEach((item) => {
+    item.value = []
+  })
+  filtersList.value.forEach((item) => {
+    pageData.value.forEach((ite) => {
+      if (item.title === ite.title) {
+        ite.value = item.value as string[]
+      }
+    })
+    placeTypeOptions.value.forEach((typeItem) => {
+      if (item.title === typeItem.title) {
+        moreList.value.push({
+          title: item.title,
+          value: item.value
+        })
+      }
+    })
+  })
+}
+
+const changePageData = (data: any, title: string) => {
+  const index = pageData.value.findIndex((item: any) => item.title === title)
+  if (index !== -1) {
+    pageData.value[index].value = cloneDeep(data)
+  }
+}
+
+const generate3UniqueIds = () => {
+  return Math.floor(Math.random() * 900000) + 100000
+}
+const addType = () => {
+  moreList.value.push({
+    title: '',
+    needKey: '',
+    id: generate3UniqueIds(),
+    value: []
+  })
+}
+
+const changeTitle = (id: any, title: string) => {
+  const needKey = placeTypeOptions.value.find((item) => item.title === title)?.needKey
+  moreList.value.forEach((item) => {
+    if (item.id === id) {
+      item.value = []
+      item.title = title
+      item.needKey = needKey
+    }
+  })
+}
+const deleteType = (id) => {
+  moreList.value = moreList.value.filter((item) => item.id !== id)
+}
+const changeData = (data: any, title: string) => {
+  const index = moreList.value.findIndex((item: any) => item.title === title)
+  if (index !== -1) {
+    moreList.value[index].value = cloneDeep(data)
+  }
+}
+
+const getQueryData = () => {
+  pageData.value.forEach((item: any) => {
+    if (item.value.length) {
+      filtersStore.updateFilter({
+        title: item.title,
+        key: item.key,
+        value: item.value,
+        keyType: 'array'
+      })
+    } else {
+      filtersStore.deleteFilterByTitle(item.title)
+    }
+  })
+  moreList.value.forEach((item) => {
+    placeTypeOptions.value.forEach((ite) => {
+      if (item.title === ite.title) {
+        item.key = ite.key
+      }
+    })
+  })
+
+  placeTypeOptions.value.forEach((item) => {
+    const index = moreList.value.findIndex((ite) => ite.title === item.title && ite.value.length)
+    if (index !== -1) {
+      const ite = moreList.value[index]
+      filtersStore.updateFilter({
+        title: ite.title,
+        key: ite.key,
+        value: ite.value,
+        keyType: 'array'
+      })
+    } else {
+      filtersStore.deleteFilterByTitle(item.title)
+    }
+  })
+}
+
+const getBadgeData = () => {
+  let count = 0
+  pageData.value.forEach((item) => {
+    if (item.value.length) {
+      count++
+    }
+  })
+  moreList.value.forEach((item) => {
+    if (item.value.length) {
+      count++
+    }
+  })
+  return count
+}
+
+const resetData = () => {
+  pageData.value.forEach((item) => {
+    item.value = []
+  })
+  moreList.value = []
+}
+
+defineExpose({
+  initData,
+  getQueryData,
+  getBadgeData,
+  resetData
+})
+</script>
+
+<template>
+  <div class="ETD" v-for="item in pageData">
+    <div class="ETD_title">{{ item.title }}</div>
+    <SelectTable :title="item.title" :data="item.value" @input="changePageData" />
+  </div>
+  <SelectTableSelect
+    :data="moreList"
+    :dateTypeoptions="placeTypeOptions"
+    @changeData="changeData"
+    @changeTitle="changeTitle"
+    @deleteType="deleteType"
+    placeholder="Please input places name"
+  >
+  </SelectTableSelect>
+  <!-- More Place Type -->
+  <div class="MoreType" @click="addType()" v-if="moreList.length !== placeTypeOptions.length">
+    <el-button class="el-button--noborder moretype">+ More Place Type</el-button>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.icon_delete {
+  fill: var(--color-danger);
+  cursor: pointer;
+}
+
+.ETD {
+  margin: 8px 16px;
+}
+
+.ETD_title {
+  font-size: var(--font-size-2);
+  color: var(--color-neutral-2);
+}
+
+.MoreType {
+  color: var(--color-accent-2);
+  padding: 0 0 16px 16px;
+  cursor: pointer;
+}
+
+:deep(.el-overlay) {
+  background-color: transparent;
+}
+
+.moretype {
+  background-color: transparent;
+  padding: 0 4px;
+  height: 24px;
+}
+</style>

+ 130 - 97
src/components/MoreFilters/src/components/SelectValue.vue

@@ -1,101 +1,68 @@
 <script setup lang="ts">
-import { ref, watch, computed } from 'vue'
 import type { DropdownInstance } from 'element-plus'
+import { cloneDeep } from 'lodash'
+import { useFiltersStore } from '@/stores/modules/filtersList'
+
+const filtersStore = useFiltersStore()
 
 interface ListItem {
   name: string
   checked: boolean
 }
 interface Props {
-  TransportListItem: ListItem[]
-  title: String
-  Serval: String
-  checkAll: boolean
-}
-const props = withDefaults(defineProps<Props>(), {
-  checkAll: false
-})
-const TransportList = ref(props.TransportListItem)
-const Title = ref(props.title)
-const Serval = ref(props.Serval)
-const checkAll = ref(props.checkAll)
-watch(
-  () => props.TransportListItem,
-  (current) => {
-    TransportList.value = current
-  }
-)
+  transportListData: ListItem[]
+  title: string
+  keyValue: string
+}
+const props = withDefaults(defineProps<Props>(), {})
+const transportList = ref(props.transportListData)
+const checkAll = ref()
+const selectData = ref()
 watch(
-  () => props.Serval,
+  () => props.transportListData,
   (current) => {
-    Serval.value = current
-  }
-)
-watch(
-  () => props.checkAll,
-  (current) => {
-    checkAll.value = current
+    transportList.value = cloneDeep(current)
+    checkAll.value = transportList.value.every((item) => {
+      return item.checked
+    })
+  },
+  {
+    immediate: true,
+    deep: true
   }
 )
 
 const dropdown1 = ref<DropdownInstance>()
-let checkedCount: any[] = []
 
-const handleCheckAllChange = (val: any) => {
-  checkedCount = []
-  TransportList.value.forEach((item: any) => {
-    if (val) {
-      item.checked = true
-      checkedCount.push(item.name)
-    } else {
-      item.checked = false
-      checkedCount = []
-    }
+const handleCheckAll = (val: any) => {
+  transportList.value.forEach((item: any) => {
+    item.checked = val
   })
 }
-const handleCheckedTransportChange = (value: any, checked: any, index: any) => {
-  if (checked) {
-    checkedCount.push(value)
-    const map = new Map()
-    checkedCount.forEach((item) => map.set(item, true))
-    checkedCount = [...map.keys()]
-  } else {
-    if (checkedCount.length == 1) {
-      checkedCount.splice(0, 1)
-    } else {
-      checkedCount.splice(index, 1)
-    }
-  }
-  checkAll.value = checkedCount.length === TransportList.value.length
+const handleCheckedTransport = () => {
+  checkAll.value = transportList.value.every((item) => item.checked)
 }
 
 // 清除选中
 const clearList = () => {
   checkAll.value = false
-  TransportList.value.forEach((item: any) => {
+  transportList.value.forEach((item: any) => {
     item.checked = false
   })
-  changedata.value = ''
-  checkedCount = []
+  selectData.value = ''
 }
-const emit = defineEmits(['generalSearch'])
-const changedata = ref()
-const changedata2 = ref()
+
 //点击搜索
 const TransportSearch = (visible: any) => {
-  if (checkedCount.length == TransportList.value.length) {
-    changedata.value = 'All'
-    changedata2.value = 'All'
+  if (checkAll.value) {
+    selectData.value = 'All'
   } else {
-    changedata.value = checkedCount.join(', ')
-    changedata2.value = checkedCount
+    selectData.value = transportList.value
+      .filter((item) => item.checked)
+      .map((item) => item.name)
+      .join(', ')
   }
-  const TransportData = {
-    title: Title.value,
-    data: changedata.value
-  }
-  Serval.value = changedata.value
-  emit('generalSearch', TransportData, changedata2.value)
+  seeall.value = false
   if (!dropdown1.value) return
   if (visible) {
     dropdown1.value.handleClose()
@@ -107,33 +74,91 @@ const seeall = ref(false)
 const clickSeeAll = () => {
   seeall.value = !seeall.value
 }
+
+const getQueryData = () => {
+  if (!selectData.value) {
+    filtersStore.deleteFilterByTitle(props.title)
+    return
+  }
+  if (checkAll.value) {
+    filtersStore.updateFilter({
+      title: props.title,
+      key: props.keyValue,
+      value: ['All'],
+      keyType: 'array'
+    })
+  } else {
+    filtersStore.updateFilter({
+      title: props.title,
+      key: props.keyValue,
+      value: selectData.value.split(', '),
+      keyType: 'array'
+    })
+  }
+}
+
+const getValue = () => {
+  return selectData.value
+}
+
+const initData = () => {
+  const filterData = filtersStore.getFilterByTitle(props.title)
+  if (filterData) {
+    if (filterData.value == 'All') {
+      transportList.value.forEach((item) => {
+        item.checked = true
+      })
+      selectData.value = 'All'
+    } else {
+      filterData.value.forEach((item) => {
+        transportList.value.forEach((item2) => {
+          if (item2.name == item) {
+            item2.checked = true
+          } else {
+            item2.checked = false
+          }
+        })
+      })
+    }
+    selectData.value = filterData.value.join(', ')
+  } else {
+    transportList.value.forEach((item) => {
+      item.checked = false
+    })
+    selectData.value = ''
+  }
+}
+
+const resetData = () => {
+  transportList.value.forEach((item) => {
+    item.checked = false
+  })
+  selectData.value = ''
+  filtersStore.deleteFilterByTitle(props.title)
+}
+defineExpose({
+  getQueryData,
+  getValue,
+  initData,
+  resetData
+})
 </script>
 <template>
-  <div class="select" :class="{ isDisabled: TransportList.length == 0 }">
+  <div class="select" :class="{ isDisabled: transportList?.length == 0 }">
     <el-dropdown
       ref="dropdown1"
       trigger="click"
       :hide-on-click="false"
-      :disabled="TransportList.length == 0"
+      :disabled="transportList?.length == 0"
     >
       <div class="el-dropdown-link">
-        <div
-          v-if="
-            props.title == 'Incoterms' && (Serval == 'Please Select Date Range' || Serval == '')
-          "
-          class="select_title"
-        >
+        <div v-if="props.title == 'Incoterms' && !selectData" class="select_title">
           Please Select Date Range
         </div>
-        <div
-          v-else-if="
-            props.title == 'Service' && (Serval == 'Please Select Service' || Serval == '')
-          "
-          class="select_title"
-        >
+        <div v-else-if="props.title == 'Service' && !selectData" class="select_title">
           Please Select Service
         </div>
-        <div v-else class="select_title_2">{{ Serval }}</div>
+        <div v-else class="select_title_2">{{ selectData }}</div>
         <span class="iconfont_icon">
           <svg class="iconfont icon_dark" aria-hidden="true">
             <use xlink:href="#icon-icon_dropdown_b"></use>
@@ -142,21 +167,21 @@ const clickSeeAll = () => {
       </div>
       <template #dropdown>
         <div class="dropdownwidth">
-          <div class="title">{{ Title }}</div>
+          <div class="title" style="margin-bottom: 3px">{{ props.title }}</div>
           <el-dropdown-menu>
             <el-dropdown-item>
-              <el-checkbox v-model="checkAll" class="checkbox" @change="handleCheckAllChange">
+              <el-checkbox v-model="checkAll" class="checkbox" @change="handleCheckAll">
                 <div class="checkbox_title">Select All</div>
               </el-checkbox>
             </el-dropdown-item>
-            <el-divider></el-divider>
+            <el-divider style="border-color: var(--input-border)"></el-divider>
             <div class="inval" :class="{ seeall: seeall }">
-              <el-dropdown-item v-for="(item, index) in TransportList" :key="index">
+              <el-dropdown-item v-for="(item, index) in transportList" :key="index">
                 <el-checkbox
                   :value="item.name"
                   v-model="item.checked"
                   class="checkbox"
-                  @change="handleCheckedTransportChange(item.name, item.checked, index)"
+                  @change="handleCheckedTransport()"
                 >
                   <div class="checkbox_title">
                     {{ item.name }}
@@ -197,23 +222,27 @@ const clickSeeAll = () => {
 .select {
   cursor: pointer;
   width: 368px;
-  height: 32px;
   display: flex;
   align-items: center;
   justify-content: space-between;
   background-color: white;
-  border: 1px solid var(--color-select-border);
-  border-radius: var(--border-radius-6);
   background-color: transparent;
+  border-radius: 6px;
 }
-.select:hover {
-  border: 1px solid var(--color-theme);
-}
+
 .el-dropdown-link {
-  width: 360px;
+  width: 368px;
+  min-height: 32px;
+  padding-top: 3px;
   display: flex;
   align-items: center;
   justify-content: space-between;
+  border-radius: var(--border-radius-6);
+  border: 1px solid var(--color-select-border);
+
+  &:hover {
+    border: 1px solid var(--color-theme);
+  }
 }
 .select_title {
   font-size: var(--font-size-3);
@@ -225,6 +254,7 @@ const clickSeeAll = () => {
   font-size: var(--font-size-3);
   font-weight: 400;
   margin-left: 8.53px;
+  line-height: 21px;
   color: var(--color-neutral-1);
 }
 .title {
@@ -291,6 +321,9 @@ const clickSeeAll = () => {
 :deep(.el-dropdown-menu__item) {
   padding: 0;
   margin: 10px 16px;
+  &:first-child {
+    margin-top: 0;
+  }
 }
 :deep(.el-dropdown-menu__item:not(.is-disabled):focus) {
   background-color: var(--border-hover-color);
@@ -330,7 +363,7 @@ const clickSeeAll = () => {
 }
 .seeall {
   max-height: 426px;
-  overflow: scroll;
+  overflow-y: auto;
 }
 .isDisabled {
   background-color: var(--input-disabled-bg-color);

+ 0 - 114
src/components/SelectTable/src/SelectTable copy.vue

@@ -1,114 +0,0 @@
-<script setup lang="ts" name="SelTable">
-
-const emit = defineEmits(['check', 'input'])
-
-const props = defineProps({
-  value: {
-    type: Array,
-    default: () => []
-  },
-  addTagOnKeys: {
-    type: Array,
-    default: () => []
-  },
-  readOnly: {
-    type: Boolean,
-    default: false
-  },
-})
-
-const tagInputRef: any = ref(null)
-const newTag = ref('')
-const innerTags = ref([...props.value])
-
-watch(
-  () => props.value,
-  () => {
-    innerTags.value = [...props.value]
-  }
-)
-
-function focusTagInput() {
-  if (props.readOnly || !tagInputRef.value) {
-    return
-  } else {
-    tagInputRef.value.focus()
-  }
-}
-
-function inputTag(ev: any) {
-  newTag.value = ev.target.value
-}
-function remove(index: number) {
-  innerTags.value.splice(index, 1)
-  tagChange()
-}
-function addNew(e: any) {
-  if (e && (!props.addTagOnKeys.includes(e.keyCode)) && (e.type !== 'blur')) {
-    return
-  }
-  if (e) {
-    e.stopPropagation()
-    e.preventDefault()
-  }
-  let addSuccess = false
-  if (newTag.value.includes(',')) {
-    newTag.value.split(',').forEach(item => {
-      if (addTag(item.trim())) {
-        addSuccess = true
-      }
-    })
-  } else {
-    if (addTag(newTag.value.trim())) {
-      addSuccess = true
-    }
-  }
-  if (addSuccess) {
-    tagChange()
-    newTag.value = ''
-  }
-}
-function addTag(tag: any) {
-  tag = tag.trim()
-  if (tag && !innerTags.value.includes(tag)) {
-    innerTags.value.push(tag)
-    return true
-  }
-  return false
-}
-
-function removeLastTag() {
-  if (newTag.value) {
-    return
-  }
-  innerTags.value.pop()
-  tagChange()
-}
-function tagChange() {
-  emit('input', innerTags.value)
-}
-
-
-</script>
-<template>
-  <div class="el-input el-input--suffix el-tooltip__trigger" @click="focusTagInput">
-    <div class="el-input__wrapper">
-      <el-space>
-        <template v-for="(tag, idx) in innerTags" :key="tag">
-          <el-tag v-bind="$attrs" type="info" size="small" round :closable="!readOnly" :disable-transitions="false"
-            @close="remove(idx)">
-            {{ tag }}
-          </el-tag>
-        </template>
-        <input v-if="!readOnly" ref="tagInputRef" class="el-input__inner" @input="inputTag" :value="newTag"
-          @keydown.delete.stop="removeLastTag" @keydown="addNew" @blur="addNew" />
-      </el-space>
-    </div>
-  </div>
-</template>
-
-<style scoped lang="scss">
-.el-input__wrapper {
-  justify-content: flex-start;
-}
-</style>

+ 172 - 156
src/components/SelectTable/src/SelectTable.vue

@@ -1,178 +1,194 @@
 <script setup lang="ts" name="SelTable">
-import _ from 'lodash'
-import { reactive, ref, onMounted, watch } from 'vue'
+import { ref, reactive, watch, onUnmounted, nextTick } from 'vue'
 import { ElMessage } from 'element-plus'
 import { formatNumber } from '@/utils/tools'
+import { useFiltersStore } from '@/stores/modules/filtersList'
+import { cloneDeep, debounce } from 'lodash'
+import { useRoute } from 'vue-router'
+import axios from 'axios'
 
-const emit = defineEmits(['check', 'input'])
+const filtersStore = useFiltersStore()
+const route = useRoute()
+const searchMode = route.path.includes('booking') ? 'booking' : 'tracking'
+
+const emit = defineEmits(['input'])
 const props = defineProps({
-  poverWidth: {
-    type: Number,
-    default: 380
-  },
-  searchInput: {
-    type: Array,
-    default: () => []
-  },
-  disabled: {
-    type: Boolean,
-    default: false
-  },
-  keyVal: {
-    type: String,
-    default: 'city'
-  },
-  isError: {
-    type: Boolean,
-    default: false
-  },
-  ASSearchMode: {
-    type: String,
-    default: ''
-  },
-  ASSearchFiled: {
-    type: String,
-    default: ''
-  },
-  ASSearchObj: {
-    type: Object
-  }
+  poverWidth: { type: Number, default: 380 },
+  data: { type: Array, default: () => [] },
+  needKey: { type: String, default: 'city' },
+  isError: { type: Boolean, default: false },
+  title: { type: String, default: '' },
+  disabled: { type: Boolean, default: false }
 })
 
-const searchVal = ref(null)
-const innerTags: any = ref([])
-const tagInputRef: any = ref(null)
+const searchVal = ref('')
+const innerTags = ref<string[]>([])
+const tagInputRef = ref<HTMLInputElement | null>(null)
+
+// 用于取消上一次搜索请求
+let searchAbortController: AbortController | null = null
 
 watch(
-  () => props.searchInput,
+  () => props.data,
   (current) => {
-    innerTags.value = current
+    innerTags.value = cloneDeep(current as string[])
   },
-  {
-    deep: true,
-    immediate: true
-  }
+  { deep: true, immediate: true }
 )
-// 响应数据
-const state: any = reactive({
+
+const state = reactive({
   poverShow: false,
   currentPage: 1,
   pageSize: 10,
   total: 0,
-  activeRowIndex: '',
   tableData: [],
   loading: false
 })
 
-// 点击行
-const handleRowClick = (row: any) => {
-  state.poverShow = false
-  if (!innerTags.value.includes(row[props.keyVal])) {
-    innerTags.value.push(row[props.keyVal])
-    state.activeRowIndex = row.id
-    emit('input', innerTags.value)
-    emit('check', innerTags.value)
-  } else {
-    ElMessage({
-      message: 'Cannot add duplicate cities.',
-      type: 'success'
-    })
+// —————— 获取数据 ——————
+const fetchData = async (query: string, page: number, isPageChange = false) => {
+  if (!isPageChange) {
+    if (searchAbortController) {
+      searchAbortController.abort()
+    }
+    searchAbortController = new AbortController()
   }
-  searchVal.value = null
-}
-const handleSearch = _.debounce((val?) => {
-  state.loading = true
-  searchVal.value = val?.target?.value
-  let filterList: any = []
-  setTimeout(() => {
-    $api
-      .getMoreFiltersTableData({
-        term: searchVal.value ? searchVal.value : '',
-        cp: state.currentPage,
-        ps: state.pageSize,
-        rc: '-1',
-        search_field: props.ASSearchFiled,
-        search_mode: props.ASSearchMode,
-        ...props.ASSearchObj
-      })
-      .then((res: any) => {
-        if (res.code == 200) {
-          filterList = res.data.searchData
-          state.loading = false
-          filterList = res.data.searchData.filter((p: any) => {
-            return (
-              p.country.toLowerCase().indexOf(searchVal.value.toLowerCase()) > -1 ||
-              p.city.toLowerCase().indexOf(searchVal.value.toLowerCase()) > -1 ||
-              p.uncode.toLowerCase().indexOf(searchVal.value.toLowerCase()) > -1
-            )
-          })
-          state.tableData = filterList
-          state.total = Number(res.data.rc)
-          state.loading = false
-        }
-      })
-  }, 800)
-}, 500)
-// 分页 请求接口
-const handleCurrentChange = () => {
+
   state.loading = true
-  let filterList: any = []
-  setTimeout(() => {
-    $api
-      .getMoreFiltersTableData({
-        term: searchVal.value ? searchVal.value : '',
-        cp: state.currentPage,
+
+  try {
+    const rc = isPageChange ? String(state.total) : '-1'
+    const queryData = filtersStore.getQueryData
+
+    const res = await $api.getMoreFiltersTableData(
+      {
+        term: query?.trim(),
+        cp: page,
         ps: state.pageSize,
-        rc: state.total,
-        search_field: props.ASSearchFiled,
-        search_mode: props.ASSearchMode,
-        ...props.ASSearchObj
-      })
-      .then((res: any) => {
-        if (res.code == 200) {
-          filterList = res.data.searchData
-          state.loading = false
-          filterList = res.data.searchData.filter((p: any) => {
-            return (
-              p.country.toLowerCase().indexOf(searchVal.value.toLowerCase()) > -1 ||
-              p.city.toLowerCase().indexOf(searchVal.value.toLowerCase()) > -1 ||
-              p.uncode.toLowerCase().indexOf(searchVal.value.toLowerCase()) > -1
-            )
-          })
-          state.tableData = filterList
-          state.total = Number(res.data.rc)
-          state.loading = false
-        }
-      })
-  }, 800)
+        rc,
+        search_field: props.title,
+        search_mode: searchMode,
+        ...queryData
+      },
+      { signal: searchAbortController?.signal }
+    )
+
+    if (searchAbortController?.signal.aborted) return
+
+    if (res.code === 200) {
+      state.tableData = res.data.searchData || []
+      state.total = Number(res.data.rc || 0)
+    } else {
+      state.tableData = []
+      state.total = 0
+    }
+  } catch (err) {
+    if (!axios.isCancel(err) && !searchAbortController?.signal.aborted) {
+      ElMessage.error('Failed to load data')
+      state.tableData = []
+      state.total = 0
+    }
+  } finally {
+    if (!searchAbortController?.signal.aborted) {
+      state.loading = false
+    }
+  }
 }
-// 删除
+
+// —————— 防抖搜索 ——————
+const debouncedSearch = debounce((query: string) => {
+  state.currentPage = 1
+  fetchData(query, 1, false)
+}, 300)
+
+// —————— 事件处理 ——————
+const handleSearchInput = () => {
+  const val = searchVal.value
+  if (val.trim() === '') {
+    state.tableData = []
+    state.total = 0
+    state.currentPage = 1
+  } else {
+    debouncedSearch(val)
+  }
+}
+
+const handlePageChange = () => {
+  fetchData(searchVal.value, state.currentPage, true)
+}
+
+const handleRowClick = (row: any) => {
+  const keyVal = row[props.needKey]
+  if (innerTags.value.includes(keyVal)) {
+    ElMessage.success('Cannot add duplicate cities.')
+    return
+  }
+
+  innerTags.value.push(keyVal)
+  emit('input', innerTags.value, props.title)
+
+  // 关闭弹窗并重置搜索
+  state.poverShow = false
+  searchVal.value = ''
+  state.tableData = []
+  state.currentPage = 1
+  state.total = 0
+}
+
 const remove = (idx: number) => {
   innerTags.value.splice(idx, 1)
-  emit('input', innerTags.value)
-  emit('check', innerTags.value)
+  emit('input', innerTags.value, props.title)
 }
 
-const onInput = () => {
-  tagInputRef.value.focus()
+// 👇 仅在点击 reference 区域时打开弹窗
+const openPopover = () => {
+  if (props.disabled) return
+  state.poverShow = true
+  nextTick(() => {
+    tagInputRef.value?.focus()
+  })
 }
+
+const clearSearchInput = () => {
+  searchVal.value = ''
+  state.tableData = []
+  state.currentPage = 1
+  state.total = -1
+  if (searchAbortController) {
+    searchAbortController.abort()
+    searchAbortController = null
+  }
+  tagInputRef.value?.blur()
+}
+
+defineExpose({ clearSearchInput })
+
+onUnmounted(() => {
+  if (searchAbortController) {
+    searchAbortController.abort()
+  }
+})
 </script>
+
 <template>
   <div>
+    <!-- 手动控制弹窗 -->
     <el-popover
       v-model:visible="state.poverShow"
       placement="bottom-start"
       :teleported="false"
       :width="props.poverWidth"
-      trigger="click"
+      trigger="manual"
+      @hide="clearSearchInput"
     >
       <template #reference>
+        <!-- 点击此区域才打开弹窗 -->
         <div
           class="el-input el-input--suffix el-tooltip__trigger"
-          @click="disabled ? null : onInput"
+          @click="openPopover"
           :class="{ is_error: props.isError }"
         >
-          <div class="el-input__wrapper" :class="{ 'is-disabled': disabled }">
+          <div class="el-input__wrapper" :class="{ 'is-disabled': props.disabled }">
             <el-space :class="[innerTags.length ? 'custom-el-spaceno' : 'custom-el-space']">
               <template v-for="(item, idx) in innerTags" :key="item">
                 <template v-if="idx <= 2">
@@ -181,7 +197,7 @@ const onInput = () => {
                   </el-tag>
                 </template>
               </template>
-              <template v-if="innerTags.length && innerTags.length > 3">
+              <template v-if="innerTags.length > 3">
                 <el-popover placement="bottom" trigger="hover">
                   <template #reference>
                     <el-tag type="info" size="small" round :disable-transitions="false">
@@ -212,19 +228,20 @@ const onInput = () => {
                 </el-popover>
               </template>
               <input
-                :disabled="props.disabled"
-                :value="searchVal"
+                v-model="searchVal"
                 ref="tagInputRef"
                 :placeholder="innerTags.length ? '' : 'Please input country/city/uncode'"
                 class="el-input__inner"
+                :disabled="props.disabled"
                 type="text"
-                @input="handleSearch"
-                @click="handleSearch"
+                @input="handleSearchInput"
+                @keydown.enter.stop.prevent
+                @focus="debouncedSearch('')"
               />
             </el-space>
-            <div class="el-input__suffix">
-              <div class="el-input__suffix-inner" v-if="!disabled">
-                <el-icon :class="state.poverShow ? 'reverse' : ''">
+            <div class="el-input__suffix" v-if="!props.disabled">
+              <div class="el-input__suffix-inner">
+                <el-icon :class="{ reverse: state.poverShow }">
                   <CaretBottom />
                 </el-icon>
               </div>
@@ -232,6 +249,8 @@ const onInput = () => {
           </div>
         </div>
       </template>
+
+      <!-- 数据表格 -->
       <el-table
         :data="state.tableData"
         border
@@ -239,11 +258,14 @@ const onInput = () => {
         v-loading="state.loading"
         @row-click="handleRowClick"
         header-row-class-name="cus-header"
+        style="width: 100%"
       >
-        <el-table-column property="country" label="Country" />
+        <el-table-column property="country" label="Country" width="75" />
         <el-table-column property="city" label="City" />
-        <el-table-column property="uncode" label="Uncode" />
+        <el-table-column property="uncode" label="Uncode" width="80" />
       </el-table>
+
+      <!-- 分页 -->
       <div class="pagination">
         <span>Total {{ formatNumber(state.total) }}</span>
         <el-pagination
@@ -253,8 +275,8 @@ const onInput = () => {
           layout="prev, pager, next"
           :pager-count="5"
           :total="state.total"
-          @current-change="handleCurrentChange"
-          @size-change="handleCurrentChange"
+          @current-change="handlePageChange"
+          @size-change="handlePageChange"
         />
       </div>
     </el-popover>
@@ -290,22 +312,16 @@ const onInput = () => {
   background-color: var(--color-table-header-bg) !important;
 }
 
-:deep(.el-table__row) {
-  td {
-    cursor: pointer;
-  }
+:deep(.el-table__row) td {
+  cursor: pointer;
 }
 
-:deep(.el-table__row:not(.current-row):hover) {
-  td {
-    background-color: var(--color-btn-default-bg-hover) !important;
-  }
+:deep(.el-table__row:not(.current-row):hover) td {
+  background-color: var(--color-btn-default-bg-hover) !important;
 }
 
-:deep(.current-row) {
-  td {
-    background-color: #ffe3cd !important;
-  }
+:deep(.current-row) td {
+  background-color: #ffe3cd !important;
 }
 
 .pagination {

+ 57 - 152
src/components/SelectTableSelect/src/SelectTableSelect.vue

@@ -2,171 +2,89 @@
 import { ref, watch } from 'vue'
 import IconDropDown from '@/components/IconDropDown'
 import SelectTable from '@/components/SelectTable'
+import { cloneDeep } from 'lodash'
 
-interface TypeItem {
-  placesType: ''
-  placesname: []
-}
-interface dateoptions {
-  value: string
-  label: string
-}
 interface Props {
-  AddDateType: TypeItem[]
-  ASPlaceholder: string
-  DateTypeoptions: dateoptions[]
-  selectedPartyTypeoptions: Array<string>
-  TablesearchTableQeury: Object
-  TablesearchMode: String
-}
-interface optionsItem {
-  value: string
-  label: string
+  data: any[]
+  placeholder: string
+  dateTypeoptions: any[]
 }
+
 const props = withDefaults(defineProps<Props>(), {})
-const AddType: any = ref([])
-const dataTypeoptions = ref<optionsItem[]>([])
-const typeSelectIndex = ref(-1)
-watch(
-  () => props.selectedPartyTypeoptions,
-  (newV) => {
-    let arr: any = []
-    if (newV.length == 0) {
-      arr = props.DateTypeoptions
-    } else {
-      props.DateTypeoptions.forEach((item: any) => {
-        let index = newV.findIndex((con: any) => {
-          return con == item.value
-        })
-        if (index < 0) {
-          arr.push(item)
-        }
-      })
-    }
-    dataTypeoptions.value = arr
-  },
-  {
-    immediate: true,
-    deep: true
-  }
-)
+
+const pageData = ref(props.data)
 
 watch(
-  () => props.AddDateType,
-  (val) => {
-    AddType.value = val
+  () => props.data,
+  (newVal) => {
+    pageData.value = cloneDeep(newVal)
   },
   {
-    immediate: true,
     deep: true
   }
 )
 
-// 删除当前more type
-const deleteType = (i: any) => {
-  emit('delSelect', i, AddType.value[i].placesType)
-  AddType.value.splice(i, 1)
-}
-// 选中type改变
-const changeSelect = (val: any) => {
-  emit('changeAutoSelectAddType', typeSelectIndex.value, val)
-}
-// 选中name改变
-const emit = defineEmits(['changeAutoSelectAddType', 'delSelect', 'changeAutoSelect'])
-const errorBoolean = ref(false)
-let AutoSelectObj: any = {}
-let AutoSelectObj2: any = {}
-const changeAutoSelect = (val: any, value: any) => {
-  if (value.length) {
-    errorBoolean.value = true
-  } else {
-    errorBoolean.value = false
-  }
-  AutoSelectObj[val] = value.join()
-  AutoSelectObj2[val] = value
-  emit('changeAutoSelect', AutoSelectObj, AutoSelectObj2, errorBoolean.value)
-}
-const typeSelectFocus = (index: any, e: any) => {
-  typeSelectIndex.value = index
-}
-const typeSelectBlur = () => {
-  typeSelectIndex.value = -1
-  let arr: any = []
-  if (props.selectedPartyTypeoptions.length == 0) {
-    arr = props.DateTypeoptions
-  } else {
-    props.DateTypeoptions.forEach((item: any) => {
-      let index = props.selectedPartyTypeoptions.findIndex((con: any) => {
-        return con == item.value
-      })
-      if (index < 0) {
-        arr.push(item)
-      }
+const originnalTypeOptions = ref(props.dateTypeoptions)
+
+// 计算排除其他已选项后的可用选项
+const getAvailableOptions = (curTitle: string) => {
+  // 获取其他下拉已选的值(排除自己)
+  const otherTypeOptions = pageData.value
+    .filter((type) => {
+      return type.title !== curTitle
     })
-  }
-  dataTypeoptions.value = arr
-}
-const typeSelectClick = (index: any, val: any) => {
-  if (AddType.value[index].placesType) {
-    let j = props.DateTypeoptions.findIndex((item: any) => {
-      return item.value == AddType.value[index].placesType
+    .map((type) => {
+      return type.title
     })
-    let i = dataTypeoptions.value.findIndex((item: any) => {
-      return item.value == AddType.value[index].placesType
+  return originnalTypeOptions.value.filter((type) => {
+    return !otherTypeOptions.includes(type.title)
+  })
+}
+
+const deleteType = (id: number) => {
+  emit('deleteType', id)
+}
+const selectTableRef = ref()
+
+// 在title变化时,清空搜索框
+const changeTitle = (title: string, item: any) => {
+  // item.value = []
+  emit('changeTitle', item.id, title)
+  nextTick(() => {
+    const index = pageData.value.findIndex((type) => {
+      return type.id === item.id
     })
-    if (i < 0) {
-      dataTypeoptions.value.push(props.DateTypeoptions[j])
-    }
-  } else {
-    let arr: any = []
-    if (props.selectedPartyTypeoptions.length == 0) {
-      arr = props.DateTypeoptions
-    } else {
-      props.DateTypeoptions.forEach((item: any) => {
-        let index = props.selectedPartyTypeoptions.findIndex((con: any) => {
-          return con == item.value
-        })
-        if (index < 0) {
-          arr.push(item)
-        }
-      })
-    }
-    dataTypeoptions.value = arr
-  }
+    selectTableRef.value[index].clearSearchInput()
+  })
 }
+const emit = defineEmits(['changeTitle', 'deleteType', 'changeData'])
 
-const checkdestination = (row: any, index: any) => {
-  if (row) {
-    AddType.value[index].placesname = row
-    changeAutoSelect(AddType.value[index].placesType, AddType.value[index].placesname)
-  }
+const changePageData = (data: any, title: string) => {
+  emit('changeData', data, title)
 }
 </script>
 <template>
-  <div class="AddType" v-for="(item, index) in AddType" :key="index">
+  <div class="AddType" v-for="item in pageData" :key="item.id">
     <div>
       <div class="Date_Title">
         <div class="ETD_title">Places Type</div>
-        <span class="iconfont_icon" @click="deleteType(index)">
+        <span class="iconfont_icon" @click="deleteType(item.id)">
           <svg class="iconfont icon_delete" aria-hidden="true">
             <use xlink:href="#icon-icon_delete_b"></use>
           </svg>
         </span>
       </div>
       <el-select
-        v-model="AddType[index].placesType"
+        :model-value="item.title"
         :suffix-icon="IconDropDown"
-        @blur="typeSelectBlur"
-        @focus="typeSelectFocus(index, $event)"
-        @click="typeSelectClick(index, $event)"
-        @change="changeSelect(AddType[index].placesType)"
+        @change="changeTitle($event, item)"
         placeholder="Please Select Party Type"
       >
         <el-option
-          v-for="item in dataTypeoptions"
-          :key="item.value"
-          :label="item.label"
-          :value="item.value"
+          v-for="type in getAvailableOptions(item.title)"
+          :key="type.title"
+          :label="type.title"
+          :value="type.title"
         >
         </el-option>
       </el-select>
@@ -174,28 +92,15 @@ const checkdestination = (row: any, index: any) => {
     <div style="margin-top: 16px">
       <div class="ETD_title">Places Details</div>
       <SelectTable
-        :ASSearchFiled="AddType[index].placesType"
-        :ASSearchObj="props.TablesearchTableQeury"
-        :ASSearchMode="props.TablesearchMode"
-        :key-val="
-          AddType[index].placesType &&
-          (AddType[index].placesType === 'Port of Loading' ||
-            AddType[index].placesType === 'Port of Discharge')
-            ? 'uncode'
-            : 'city'
-        "
-        :is-error="
-          AddType[index].placesType != '' && AddType[index].placesname.length == 0 ? true : false
-        "
-        :disabled="AddType[index].placesType ? false : true"
-        :searchInput="AddType[index].placesname"
-        @check="(row) => checkdestination(row, index)"
+        ref="selectTableRef"
+        :title="item.title"
+        :data="item.value"
+        :needKey="item.needKey"
+        :disabled="!item.title ? true : false"
+        @input="changePageData"
       />
 
-      <div
-        class="error"
-        v-if="AddType[index].placesType != '' && AddType[index].placesname.length == 0"
-      >
+      <div class="error" v-if="item.title != '' && item.value.length === 0">
         Please Input Places Details
       </div>
     </div>

+ 49 - 175
src/components/TransportMode/src/TransportMode.vue

@@ -1,8 +1,9 @@
 <script setup lang="ts">
-import { ref, onMounted, onBeforeMount, watch, computed } from 'vue'
 import type { DropdownInstance } from 'element-plus'
-import emitter from '@/utils/bus'
 import { cloneDeep } from 'lodash'
+import { useFiltersStore } from '@/stores/modules/filtersList'
+
+const filtersStore = useFiltersStore()
 
 interface ListItem {
   name: string
@@ -12,203 +13,76 @@ interface ListItem {
   checked: boolean
 }
 interface Props {
-  TransportListItem: ListItem[]
-  isShipment: Boolean
+  transportListItem: ListItem[]
 }
 const props = withDefaults(defineProps<Props>(), {})
-const TransportList = ref(props.TransportListItem)
+const transportList = ref()
+const checkAll = ref(false)
+const dropdownVisible = ref<DropdownInstance>()
 const updateTransportList = (data: any) => {
-  TransportList.value = cloneDeep(data)
-  TransportList.value.forEach((item: any) => {
-    if (item.checked) {
-      checkedCount.push(item.sname)
-      const map = new Map()
-      checkedCount.forEach((item) => map.set(item, true))
-      checkedCount = [...map.keys()]
-    }
-    if (checkedCount.length == TransportList.value.length) {
-      checkAll.value = true
-    }
-  })
+  transportList.value = cloneDeep(data)
+  const isCheckedAll = transportList.value?.every((item: any) => item.checked)
+  isCheckedAll ? (checkAll.value = true) : (checkAll.value = false)
 }
 watch(
-  () => props.TransportListItem,
+  () => props.transportListItem,
   (current) => {
     updateTransportList(current)
+  },
+  {
+    immediate: true,
+    deep: true
   }
 )
 
-onMounted(() => {
-  emitter.on('clearTag', (tag: any) => {
-    if (tag.includes('Transport Mode')) {
-      clearList()
-    }
-  })
-  defaultTransport()
-})
-
-onBeforeMount(() => {
-  emitter.off('clearTag')
-})
-
-const checkAll = ref(false)
-const dropdownVisible = ref<DropdownInstance>()
-let checkedCount: any[] = []
-const TotalAll = computed(() => {
-  var total_sum = 0
-  if (TransportList.value != undefined) {
-    TransportList.value.map((item) => {
-      total_sum += item.number
-    })
-  }
-  return total_sum
+const totalNumber = computed(() => {
+  return (
+    transportList.value?.filter((item) => {
+      return item.checked
+    }).length || 0
+  )
 })
 
-const handleCheckAllChange = (val: any) => {
-  checkedCount = []
-  TransportList.value.forEach((item: any) => {
-    if (val) {
-      item.checked = true
-      checkedCount.push(item.sname)
-    } else {
-      item.checked = false
-      checkedCount = []
-    }
+const handleCheckAllChange = (checked: any) => {
+  transportList.value.forEach((item: any) => {
+    item.checked = checked
   })
 }
-const handleCheckedTransportChange = (value: any, checked: any) => {
-  if (checked) {
-    checkedCount.push(value)
-    const map = new Map()
-    checkedCount.forEach((item) => map.set(item, true))
-    checkedCount = [...map.keys()]
-  } else {
-    checkedCount.splice(checkedCount.indexOf(value), 1)
-  }
-  checkAll.value = checkedCount.length === TransportList.value.length
+const handleCheckedTransportChange = (item: any) => {
+  const isCheckedAll = transportList.value.every((item: any) => item.checked)
+  isCheckedAll ? (checkAll.value = true) : (checkAll.value = false)
 }
 
 // 清除选中
 const clearList = () => {
   checkAll.value = false
-  if (TransportList.value != undefined) {
-    TransportList.value.forEach((item: any) => {
+  if (transportList.value != undefined) {
+    transportList.value.forEach((item: any) => {
       item.checked = false
     })
   }
-  changedata.value = ''
-  checkedCount = []
-  emit('clearTransportTags')
-}
-const emit = defineEmits(['TransportSearch', 'clearTransportTags', 'defaultTransport'])
-const changedata = ref()
-//点击搜索
-const TransportData = {
-  title: 'Transport Mode',
-  data: ['']
-}
-const TransportSearch = (visible: any) => {
-  TransportList.value.forEach((item: any) => {
-    if (item.checked) {
-      checkedCount.push(item.sname)
-      const map = new Map()
-      checkedCount.forEach((item) => map.set(item, true))
-      checkedCount = [...map.keys()]
-    }
-  })
-  if (checkedCount.length == TransportList.value.length) {
-    changedata.value = ['All']
-  } else {
-    changedata.value = checkedCount
-  }
-  TransportData.data = changedata.value
-  emit('TransportSearch', TransportData)
-  if (!dropdownVisible.value) return
-  if (visible) {
-    dropdownVisible.value.handleClose()
-  } else {
-    dropdownVisible.value.handleOpen()
-  }
 }
-const searchTableQeury = ref()
-const searchTableQeuryTracking = ref()
-const defaultTransport = () => {
-  if (props.isShipment) {
-    if (
-      sessionStorage.getItem('clickParams') == null ||
-      sessionStorage.getItem('clickParams') == '{}'
-    ) {
-      if (sessionStorage.getItem('searchTableQeuryTracking') == null) {
-        checkAll.value = true
-        TransportData.data = ['All']
-      } else {
-        searchTableQeuryTracking.value =
-          JSON.parse(sessionStorage.getItem('searchTableQeuryTracking') as string) || {}
-        if (searchTableQeuryTracking.value.transport_mode != undefined) {
-          if (searchTableQeuryTracking.value.transport_mode.length !== 0) {
-            TransportData.data = searchTableQeuryTracking.value.transport_mode
-            searchTableQeuryTracking.value.transport_mode.forEach((item: any) => {
-              if (item == 'All') {
-                checkAll.value = true
-              } else {
-                checkAll.value = false
-              }
-            })
-          }
-        } else {
-          TransportData.data = []
-          checkAll.value = false
-        }
-      }
-      emit('defaultTransport', TransportData, searchTableQeuryTracking.value)
-    } else {
-      const data = JSON.parse(sessionStorage.getItem('reportList') as string) || {}
-      searchTableQeuryTracking.value =
-        JSON.parse(sessionStorage.getItem('searchTableQeuryTracking') as string) || {}
-      if (data.transport_mode) {
-        const obj = {
-          title: 'Transport Mode',
-          data: data.transport_mode
-        }
-        data.transport_mode.forEach((item: any) => {
-          if (item == 'All') {
-            checkAll.value = true
-          }
-        })
-        emit('defaultTransport', obj, searchTableQeuryTracking.value)
-      }
-    }
-  } else {
-    if (sessionStorage.getItem('searchTableQeury') == null) {
-      checkAll.value = true
-      TransportData.data = ['All']
-    } else {
-      searchTableQeury.value =
-        JSON.parse(sessionStorage.getItem('searchTableQeury') as string) || {}
-      if (searchTableQeury.value.transport_mode !== undefined) {
-        if (searchTableQeury.value.transport_mode.length !== 0) {
-          TransportData.data = searchTableQeury.value.transport_mode
-          searchTableQeury.value.transport_mode.forEach((item: any) => {
-            if (item == 'All') {
-              checkAll.value = true
-            } else {
-              checkAll.value = false
-            }
-          })
-        }
-      } else {
-        TransportData.data = []
-        checkAll.value = false
-      }
-    }
-    emit('defaultTransport', TransportData, searchTableQeury.value)
-  }
+const emit = defineEmits(['transportSearch'])
+
+const transportSearch = () => {
+  const allChecked = transportList.value.every((item) => item.checked)
+  const selectedNames = allChecked
+    ? ['All']
+    : transportList.value.filter((item) => item.checked).map((item) => item.sname)
+  filtersStore.updateFilter({
+    title: 'Transport Mode',
+    value: selectedNames,
+    keyType: 'array',
+    key: 'transport_mode'
+  })
+  dropdownVisible.value.handleClose()
+  emit('transportSearch', transportList.value)
 }
 
 // 每次打开时都应该重新赋值
 const handleDropdownVisibleChange = (visible: boolean) => {
   if (visible) {
-    updateTransportList(props.TransportListItem)
+    updateTransportList(props.transportListItem)
   }
 }
 </script>
@@ -242,16 +116,16 @@ const handleDropdownVisibleChange = (visible: boolean) => {
                   </span>
                   Select All
                 </div>
-                <div class="checkbox_number">({{ TotalAll }})</div>
+                <div class="checkbox_number">({{ totalNumber }})</div>
               </el-checkbox>
             </el-dropdown-item>
             <el-divider></el-divider>
-            <el-dropdown-item v-for="(item, index) in TransportList" :key="index">
+            <el-dropdown-item v-for="(item, index) in transportList" :key="index">
               <el-checkbox
                 :value="item.name"
                 v-model="item.checked"
                 class="checkbox"
-                @change="handleCheckedTransportChange(item.sname, item.checked)"
+                @change="handleCheckedTransportChange(item)"
               >
                 <div class="checkbox_title">
                   <span class="iconfont_icon">
@@ -269,7 +143,7 @@ const handleDropdownVisibleChange = (visible: boolean) => {
                 <el-button class="clear" type="default" @click="clearList">Reset</el-button>
               </div>
               <div>
-                <el-button class="search el-button--dark" @click="TransportSearch"
+                <el-button class="search el-button--dark" @click="transportSearch"
                   >Search</el-button
                 >
               </div>

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

@@ -57,11 +57,11 @@ const jumpLinkMonitoring = () => {
     <template v-for="(routeItem, index) in breadCrumb.routeList" :key="routeItem.label">
       <template v-if="index + 1 !== breadCrumb.routeList.length">
         <span @click="jumpLink(routeItem.label, routeItem.query)" class="previous-route">{{
-          routeItem.label
+          routeItem.breadName
         }}</span>
         <span class="interval">|</span>
       </template>
-      <span v-else>{{ routeItem.label }}</span>
+      <span v-else>{{ routeItem.breadName }}</span>
     </template>
   </div>
   <div v-else></div>

+ 10 - 10
src/components/VTag/src/VTag.vue

@@ -26,10 +26,10 @@ const mappingTable = new Map([
   ['Created', 'created'],
   ['Booked', 'booked'],
   ['Cargo Received', 'cargo-received'],
-  ['Departure', 'departure'],
+  // ['Departure', 'departure'],
   ['Arrived', 'arrived'],
   ['Completed', 'completed'],
-  ['Departed', 'Departed'],
+  ['Departed', 'departed'],
   ['Pending Approval', 'pending-approval'],
   ['Active', 'active'],
   ['Inactive', 'inactive']
@@ -104,7 +104,7 @@ defineProps<internalProps>()
       background-color: var(--color-tag-cargo-received);
     }
   }
-  &.v-tag__departure {
+  &.v-tag__departed {
     background-color: var(--color-tag-departure-bg);
     color: var(--color-tag-departure);
     .dot {
@@ -125,13 +125,13 @@ defineProps<internalProps>()
       background-color: var(--color-tag-completed);
     }
   }
-  &.v-tag__Departed {
-    background-color: var(--color-tag-Departed-bg);
-    color: var(--color-tag-Departed);
-    .dot {
-      background-color: var(--color-tag-Departed);
-    }
-  }
+  // &.v-tag__Departed {
+  //   background-color: var(--color-tag-Departed-bg);
+  //   color: var(--color-tag-Departed);
+  //   .dot {
+  //     background-color: var(--color-tag-Departed);
+  //   }
+  // }
   &.v-tag__pending-approval {
     background-color: var(--color-tag-unfinished-approval-bg);
     color: var(--color-tag-unfinished-approval);

+ 175 - 194
src/components/selectAutoSelect/src/selectAutoSelect.vue

@@ -1,251 +1,232 @@
 <script setup lang="ts">
-import { ref, watch } from 'vue'
+import { ref, watch, onUnmounted } from 'vue'
 import IconDropDown from '@/components/IconDropDown'
-interface TypeItem {
-  partyType: ''
-  partyname: []
-}
-interface ListItem {
-  value: string
-  label: string
-}
-interface dateoptions {
-  value: string
-  label: string
-}
+import { cloneDeep, debounce } from 'lodash'
+import { useFiltersStore } from '@/stores/modules/filtersList'
+import { useRoute } from 'vue-router'
+import axios from 'axios'
+
+const route = useRoute()
+const searchMode = route.path.includes('booking') ? 'booking' : 'tracking'
+const filtersStore = useFiltersStore()
+
 interface Props {
-  AddDateType: TypeItem[]
-  ASPlaceholder: string
-  DateTypeoptions: dateoptions[]
-  selectedPartyTypeoptions: Array<string>
-  ASSearchMode: String
-  ASSearchObj: Object
-}
-interface optionsItem {
-  value: string
-  label: string
-  checked: boolean
+  placeholder: string
+  typeOptions: Array<any>
+  data: Array<any>
 }
 
-const list = ref<ListItem[]>([])
-const options = ref<ListItem[]>([])
-const loading = ref(false)
 const props = withDefaults(defineProps<Props>(), {})
-const AddType = ref(props.AddDateType)
-watch(
-  () => props.AddDateType,
-  (val) => {
-    AddType.value = val
-  },
-  {
-    immediate: true,
-    deep: true
-  }
+
+// 每个 item 的独立状态:loading、options、abortController
+const itemStates = ref(
+  new Map<
+    string,
+    {
+      loading: boolean
+      options: Array<{ value: string; label: string; checked: boolean }>
+      controller: AbortController | null
+    }
+  >()
 )
-// const ErrorNumber = ref(0)
-const dataTypeoptions = ref<optionsItem[]>([])
-const typeSelectIndex = ref(-1)
+
+const pageData = ref(cloneDeep(props.data))
+
 watch(
-  () => props.selectedPartyTypeoptions,
-  (newV) => {
-    let arr: any = []
-    if (newV.length == 0) {
-      arr = props.DateTypeoptions
-    } else {
-      props.DateTypeoptions.forEach((item: any) => {
-        let index = newV.findIndex((con: any) => {
-          return con == item.value
+  () => props.data,
+  (newVal) => {
+    pageData.value = cloneDeep(newVal)
+    // 初始化每个 item 的状态(如果不存在)
+    newVal.forEach((item) => {
+      if (!itemStates.value.has(item.title)) {
+        itemStates.value.set(item.title, {
+          loading: false,
+          options: [],
+          controller: null
         })
-        if (index < 0) {
-          arr.push(item)
-        }
-      })
-    }
-    dataTypeoptions.value = arr
+      }
+    })
   },
-  {
-    immediate: true,
-    deep: true
-  }
+  { deep: true, immediate: true }
 )
-const ErrorNumber = ref(false)
-const str = ref()
-const search_field = ref()
-const InputAutoSelect = (val: any) => {
-  search_field.value = val
-  if (val.includes('Agent')) {
-    str.value = 'apex'
-  } else {
-    str.value = 'sales'
-  }
-}
-const remoteMethod = (query: string) => {
-  if (query) {
-    loading.value = true
-    setTimeout(() => {
-      $api
-        .getMoreFiltersData({
-          term: query,
-          type: str.value,
-          search_field: search_field.value,
-          search_mode: props.ASSearchMode,
-          ...props.ASSearchObj
-        })
-        .then((res: any) => {
-          if (res.code == 200) {
-            loading.value = false
-            list.value = res.data.map((item: any) => {
-              return { value: item, label: item, checked: testAuto.value?.includes(item) }
-            })
-            options.value = list.value.filter((item) => {
-              return item.label.toLowerCase().includes(query.toLowerCase())
-            })
-          }
-        })
-    }, 200)
-  } else {
-    options.value = []
-  }
+
+const originnalTypeOptions = ref(props.typeOptions)
+
+// 计算排除其他已选项后的可用选项
+const getAvailableOptions = (curTitle: string) => {
+  const otherTypeOptions = pageData.value
+    .filter((type) => type.title !== curTitle)
+    .map((type) => type.title)
+  return originnalTypeOptions.value.filter((type) => !otherTypeOptions.includes(type.title))
 }
-// 删除当前more type
-const deleteType = (i: any) => {
-  emit('delSelect', i, AddType.value[i].partyType)
-  AddType.value.splice(i, 1)
+
+const emit = defineEmits(['deleteItem', 'changeTitle', 'changeValue'])
+
+const changeTitle = (title: string, item: any) => {
+  emit('changeTitle', item, title)
 }
-// 选中type改变
-const changeSelect = (val: any) => {
-  emit('changeAutoSelectAddType', typeSelectIndex.value, val)
+
+const changeValue = (value: string[], item: any) => {
+  emit('changeValue', item, value)
 }
-// 选中name改变
-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) {
-    ErrorNumber.value = true
-  } else {
-    ErrorNumber.value = false
+
+const deleteItem = (title: string) => {
+  // 清理该 item 的状态和 abort pending request
+  const state = itemStates.value.get(title)
+  if (state?.controller) {
+    state.controller.abort()
   }
-  emit('changeAutoSelect', AutoSelectObj, AutoSelectObj2, ErrorNumber.value)
-}
-const typeSelectFocus = (index: any, e: any) => {
-  typeSelectIndex.value = index
+  itemStates.value.delete(title)
+  emit('deleteItem', title)
 }
-const typeSelectBlur = () => {
-  typeSelectIndex.value = -1
-  let arr: any = []
-  if (props.selectedPartyTypeoptions.length == 0) {
-    arr = props.DateTypeoptions
-  } else {
-    props.DateTypeoptions.forEach((item: any) => {
-      let index = props.selectedPartyTypeoptions.findIndex((con: any) => {
-        return con == item.value
-      })
-      if (index < 0) {
-        arr.push(item)
-      }
-    })
+
+const visibleChange = (val: boolean, title: string) => {
+  if (!val) {
+    const state = itemStates.value.get(title)
+    if (state) {
+      state.options = []
+    }
   }
-  dataTypeoptions.value = arr
 }
-const typeSelectClick = (index: any, val: any) => {
-  if (AddType.value[index].partyType) {
-    let j = props.DateTypeoptions.findIndex((item: any) => {
-      return item.value == AddType.value[index].partyType
-    })
-    let i = dataTypeoptions.value.findIndex((item: any) => {
-      return item.value == AddType.value[index].partyType
-    })
-    if (i < 0) {
-      dataTypeoptions.value.push(props.DateTypeoptions[j])
+
+// 创建带防抖 + 竞态处理的远程搜索函数
+const createQueryHandler = (title: string, curValue: string[]) => {
+  const debouncedSearch = debounce((query: string) => {
+    const state = itemStates.value.get(title)
+    if (!state) return
+
+    // 取消上一次请求
+    if (state.controller) {
+      state.controller.abort()
     }
-  } else {
-    let arr: any = []
-    if (props.selectedPartyTypeoptions.length == 0) {
-      arr = props.DateTypeoptions
-    } else {
-      props.DateTypeoptions.forEach((item: any) => {
-        let index = props.selectedPartyTypeoptions.findIndex((con: any) => {
-          return con == item.value
-        })
-        if (index < 0) {
-          arr.push(item)
+
+    if (!query.trim()) {
+      state.options = []
+      state.loading = false
+      return
+    }
+
+    const newController = new AbortController()
+    state.controller = newController
+    state.loading = true
+
+    const curType = originnalTypeOptions.value.find((type) => type.title === title)
+    if (!curType) {
+      state.loading = false
+      return
+    }
+
+    const queryData = filtersStore.getQueryData
+    $api
+      .getMoreFiltersData(
+        {
+          term: query,
+          type: curType.type,
+          search_field: title,
+          search_mode: searchMode,
+          ...queryData
+        },
+        { signal: newController.signal }
+      )
+      .then((res: any) => {
+        if (!newController.signal.aborted && res?.code === 200) {
+          state.options = (res.data || []).map((item: string) => ({
+            value: item,
+            label: item,
+            checked: curValue?.includes(item)
+          }))
         }
       })
-    }
-    dataTypeoptions.value = arr
-  }
+      .catch((err) => {
+        if (!axios.isCancel(err) && !newController.signal.aborted) {
+          // ElMessage.error('Failed to load options')
+        }
+        if (itemStates.value.has(title)) {
+          itemStates.value.get(title)!.options = []
+        }
+      })
+      .finally(() => {
+        // 仅当这是最新 controller 时才关闭 loading
+        if (
+          itemStates.value.has(title) &&
+          itemStates.value.get(title)?.controller === newController
+        ) {
+          state.loading = false
+        }
+      })
+  }, 200)
+
+  return debouncedSearch
 }
+
+// 组件卸载时取消所有 pending 请求
+onUnmounted(() => {
+  itemStates.value.forEach((state) => {
+    if (state.controller) {
+      state.controller.abort()
+    }
+  })
+})
 </script>
+
 <template>
-  <div class="AddType" v-for="(item, index) in AddType" :key="index">
+  <div class="addType" v-for="item in pageData" :key="item.id">
     <div>
       <div class="Date_Title">
         <div class="ETD_title">Party Type</div>
-        <span class="iconfont_icon" @click="deleteType(index)">
+        <span class="iconfont_icon" @click="deleteItem(item.title)">
           <svg class="iconfont icon_delete" aria-hidden="true">
             <use xlink:href="#icon-icon_delete_b"></use>
           </svg>
         </span>
       </div>
       <el-select
-        v-model="AddType[index].partyType"
+        :model-value="item.title"
         :suffix-icon="IconDropDown"
-        @blur="typeSelectBlur"
-        @focus="typeSelectFocus(index, $event)"
-        @click="typeSelectClick(index, $event)"
-        @change="changeSelect(AddType[index].partyType)"
+        @change="changeTitle($event, item)"
         placeholder="Please Select Party Type"
       >
         <el-option
-          v-for="item in dataTypeoptions"
-          :key="item.value"
-          :label="item.label"
-          :value="item.value"
-        >
-        </el-option>
+          v-for="type in getAvailableOptions(item.title)"
+          :key="type.title"
+          :label="type.title"
+          :value="type.title"
+        />
       </el-select>
     </div>
     <div style="margin-top: 16px">
       <div class="ETD_title">Party Details</div>
       <el-select
-        v-model="AddType[index].partyname"
+        :model-value="item.value"
         multiple
         filterable
         remote
-        :class="{
-          is_error: AddType[index].partyType != '' && AddType[index].partyname.length == 0
-        }"
+        :class="{ is_error: item.value.length > 0 && item.title }"
         reserve-keyword
-        :placeholder="props.ASPlaceholder"
+        :placeholder="props.placeholder"
         collapse-tags
-        @input="InputAutoSelect(AddType[index].partyType)"
-        @change="changeAutoSelect(AddType[index].partyType, AddType[index].partyname)"
-        :disabled="AddType[index].partyType ? false : true"
+        :disabled="!item.title"
         collapse-tags-tooltip
         :max-collapse-tags="3"
-        :remote-method="remoteMethod"
-        :loading="loading"
+        :remote-method="createQueryHandler(item.title, item.value)"
+        :loading="itemStates.get(item.title)?.loading ?? false"
+        @change="changeValue($event, item)"
+        @visible-change="(val) => visibleChange(val, item.title)"
       >
         <el-option
-          v-for="item in options"
-          :key="item.value"
-          :label="item.label"
-          :value="item.value"
+          v-for="infoItem in itemStates.get(item.title)?.options || []"
+          :key="infoItem.label"
+          :label="infoItem.label"
+          :value="infoItem.label"
         >
-          <el-checkbox :checked="item.checked">
-            <span class="label" @click="item.checked = !item.checked">{{ item.value }}</span>
+          <el-checkbox :checked="infoItem.checked" style="flex: 1">
+            <span class="label" @click="infoItem.checked = !infoItem.checked">{{
+              infoItem.label
+            }}</span>
           </el-checkbox>
         </el-option>
       </el-select>
-      <div
-        class="error"
-        v-if="AddType[index].partyType != '' && AddType[index].partyname.length == 0"
-      >
+      <div class="error" v-if="item.value.length === 0 && item.title">
         Please Input Party Details
       </div>
     </div>
@@ -253,7 +234,7 @@ const typeSelectClick = (index: any, val: any) => {
 </template>
 
 <style lang="scss" scoped>
-.AddType {
+.addType {
   background-color: var(--addparties-background-color);
   margin: 0 16px 8px 16px;
   padding: 8px;

+ 3 - 8
src/router/index.ts

@@ -1,6 +1,7 @@
 import { createRouter, createWebHistory } from 'vue-router'
 import { useUserStore } from '@/stores/modules/user'
 import { useBreadCrumb } from '@/stores/modules/breadCrumb'
+import { useHeaderSearch } from '@/stores/modules/headerSearch'
 
 const router = createRouter({
   history: createWebHistory(`${import.meta.env.VITE_BASE_URL}`),
@@ -212,14 +213,6 @@ const router = createRouter({
           }
         },
         {
-          path: '/destination-delivery/modify-booking',
-          name: 'Modify Booking',
-          component: () => import('../views/DestinationDelivery/src/components/ModifyBooking'),
-          meta: {
-            breadName: 'Modify Booking',
-            activeMenu: '/destination-delivery'
-          }
-        }, {
           path: '/demo-video',
           name: 'Demo Video',
           component: () => import('../views/Video'),
@@ -233,10 +226,12 @@ const router = createRouter({
 router.beforeEach(async (to, from, next) => {
   useBreadCrumb().setRouteList(to)
   const userStore = useUserStore()
+  const headerSearchStore = useHeaderSearch()
   // 如果手动跳转登录页,清除登录信息
   if (to.path === '/login') {
     if (userStore.isLogin) {
       await userStore.logout()
+      headerSearchStore.clearSearchData()
     }
     sessionStorage.removeItem('trackingTablePageInfo')
     sessionStorage.removeItem('bookingTablePageInfo')

+ 18 - 10
src/stores/modules/breadCrumb.ts

@@ -1,7 +1,8 @@
 import { defineStore } from 'pinia'
 
 interface Route {
-  label: string
+  breadName: string
+
   path: string
   query?: string
 }
@@ -43,44 +44,49 @@ export const useBreadCrumb = defineStore('breadCrumb', {
         return
       }
       if (toRoute.name === 'Destination Create New Rule') {
-        let label = ''
+        let breadName = ''
         if (toRoute.query.a != undefined) {
-          label = 'Modify Rule'
+          breadName = 'Modify Rule'
         } else {
-          label = 'Create New Rule'
+          breadName = 'Create New Rule'
         }
         this.routeList = [
           {
+            breadName: 'Destination Delivery',
             label: 'Destination Delivery',
             path: '/destination-delivery',
             query: ''
           },
           {
+            breadName: 'Configurations',
             label: 'Configurations',
             path: '/destination-delivery/configurations',
             query: ''
           },
           {
-            label: label,
+            breadName: breadName,
+            label: toRoute.name,
             path: '/destination-delivery/create-new-rule',
             query: toRoute.query
           }
         ]
       } else if (toRoute.name === 'Create New Booking') {
-        let label = ''
+        let breadName = ''
         if (toRoute.query.a != undefined) {
-          label = 'Modify Booking'
+          breadName = 'Modify Booking'
         } else {
-          label = 'Create New Booking'
+          breadName = 'Create New Booking'
         }
         this.routeList = [
           {
+            breadName: 'Destination Delivery',
             label: 'Destination Delivery',
             path: '/destination-delivery',
             query: ''
           },
           {
-            label: label,
+            breadName: breadName,
+            label: toRoute.name,
             path: '/destination-delivery/create-new-booking',
             query: toRoute.query
           }
@@ -88,7 +94,8 @@ export const useBreadCrumb = defineStore('breadCrumb', {
 
       } else if (toRoute.name && whiteList.includes(toRoute.name)) {
         this.routeList.push({
-          label: toRoute?.meta?.breadName || toRoute.name,
+          breadName: toRoute?.meta?.breadName || toRoute.name,
+          label: toRoute.name,
           path: toRoute.path,
           query: toRoute.query
         })
@@ -96,6 +103,7 @@ export const useBreadCrumb = defineStore('breadCrumb', {
         this.routeList = [
           {
             label: toRoute.name,
+            breadName: toRoute?.meta?.breadName || toRoute.name,
             path: toRoute.path,
             query: toRoute.query
           }

+ 115 - 0
src/stores/modules/filtersList.ts

@@ -0,0 +1,115 @@
+import { defineStore } from 'pinia'
+import dayjs from 'dayjs'
+import { useUserStore } from '@/stores/modules/user'
+
+const valueFormat = 'MM/DD/YYYY'
+
+interface StateType {
+  filtersList: FiltersType[]
+}
+
+export type FiltersType =
+  | {
+    title: string
+    key: string
+    keyType: 'normal'
+    value: string
+    isHide?: boolean
+  }
+  | {
+    title: string
+    key: string
+    keyType: 'array'
+    value: string[]
+    isHide?: boolean
+  } | {
+    title: string
+    key: string[]
+    keyType: 'dateRange'
+    value: string[]
+    isHide?: boolean
+  }
+
+export const useFiltersStore = defineStore('filtersStore', {
+  state: (): StateType => ({ filtersList: [] }),
+
+  getters: {
+    getQueryData(state) {
+      const userStore = useUserStore()
+      const formatDate = userStore.dateFormat
+      return state.filtersList.reduce((acc, cur) => {
+        if (cur.keyType === 'normal') {
+          acc[cur.key] = cur.value
+        } else if (cur.keyType === 'array') {
+          acc[cur.key] = cur.value
+        } else if (cur.keyType === 'dateRange') {
+          acc[cur.key[0]] = cur.isHide ? cur.value[0] : dayjs(cur.value[0], formatDate).format(valueFormat)
+          acc[cur.key[1]] = cur.isHide ? cur.value[1] : dayjs(cur.value[1], formatDate).format(valueFormat)
+        }
+        return acc
+      }, {})
+    },
+    getTagsList(state) {
+      return state.filtersList
+        .filter(item => !item.isHide)
+        .map(filter => {
+          let displayValue: string
+          if (filter.keyType === 'dateRange') {
+            displayValue = `${filter.value[0]} To ${filter.value[1]}`
+          } else if (filter.keyType === 'array') {
+            displayValue = filter.value.join(', ')
+          } else {
+            displayValue = String(filter.value) // 防止 value 是 number/null
+          }
+          return {
+            title: filter.title,
+            value: `${filter.title}: ${displayValue}`
+          }
+        })
+    }
+  },
+  actions: {
+    getFilterIndexByKey(key: string) {
+      return this.filtersList.findIndex(filter => {
+        return filter.key === key
+      })
+    },
+    getFilterIndexByTitle(title: string) {
+      return this.filtersList.findIndex(filter => {
+        return filter.title === title
+      })
+    },
+    getFilterByKey(key: string) {
+      return this.filtersList.find(filter => filter.key === key)
+    },
+    getFilterByTitle(title: string) {
+      return this.filtersList.find(filter => filter.title === title)
+    },
+    deleteFilterByKey(key: string) {
+      const index = this.getFilterIndexByKey(key)
+      if (index !== -1) {
+        this.filtersList.splice(index, 1)
+      }
+    },
+    deleteFilterByTitle(title: string) {
+      const index = this.getFilterIndexByTitle(title)
+      if (index !== -1) {
+        this.filtersList.splice(index, 1)
+      }
+    },
+    updateFilter(filter: FiltersType) {
+      const index = this.getFilterIndexByTitle(filter.title)
+      if (index !== -1) {
+        this.filtersList[index] = filter
+      } else {
+        this.filtersList.push(filter)
+      }
+    },
+    clearFilters() {
+      this.filtersList = []
+    }
+  },
+  persist: {
+    storage: sessionStorage // 👈 使用 sessionStorage
+  }
+})

+ 0 - 18
src/stores/modules/loadingState.ts

@@ -1,18 +0,0 @@
-import { defineStore } from 'pinia'
-
-interface LoadingState {
-  trackingTableLoading: boolean
-}
-
-export const useLoadingState = defineStore('loadingState', {
-  state: (): LoadingState => ({
-    trackingTableLoading: JSON.parse(localStorage.getItem('trackingTableLoading')) || false
-  }),
-  getters: {},
-  actions: {
-    setTrackingTableLoading(state: boolean) {
-      localStorage.setItem('trackingTableLoading', JSON.stringify(state))
-      this.trackingTableLoading = state
-    }
-  }
-})

+ 65 - 27
src/styles/Antdui.scss

@@ -24,8 +24,8 @@
 .ant-picker-dropdown .ant-picker-cell-in-view.ant-picker-cell-selected .ant-picker-cell-inner,
 .ant-picker-dropdown .ant-picker-cell-in-view.ant-picker-cell-range-start .ant-picker-cell-inner,
 .ant-picker-dropdown .ant-picker-cell-in-view.ant-picker-cell-range-end .ant-picker-cell-inner {
-  background-color: var(--color-theme);
-  color: #ffffff;
+  background-color: var(--color-theme) !important;
+  color: #ffffff !important;
 }
 .ant-picker-dropdown .ant-picker-cell-in-view.ant-picker-cell-in-range::before {
   background-color: var(--color-orange-6);
@@ -43,13 +43,29 @@
 .ant-picker-dropdown .ant-picker-cell-in-view.ant-picker-cell-in-range {
   color: var(--color-theme);
 }
-.ant-picker-cell:hover:not(.ant-picker-cell-in-view).ant-picker-cell-inner,
-.ant-picker-cell:hover:not(.ant-picker-cell-selected):not(.ant-picker-cell-range-start)
-  :not(.ant-picker-cell-range-end):not(.ant-picker-cell-range-hover-start):not(
-    .ant-picker-cell-range-hover-end
-  ) {
+
+.ant-picker-dropdown .ant-picker-cell::before {
+  height: 32px;
+}
+.ant-picker-cell.ant-picker-cell-in-view:hover .ant-picker-cell-inner {
   background-color: var(--color-orange-6) !important;
+  color: var(--color-theme) !important;
+}
+.ant-picker-dropdown .ant-picker-cell:hover:not(.ant-picker-cell-selected):not(.ant-picker-cell-range-start):not(.ant-picker-cell-range-end):not(.ant-picker-cell-range-hover-start):not(.ant-picker-cell-range-hover-end) .ant-picker-cell-inner {
+  background-color: var(--color-orange-6);
+}
+.ant-picker-dropdown .ant-picker-cell .ant-picker-cell-inner {
+  height: 32px;
+  width: 32px;
+  margin: 0 auto;
+}
+.ant-picker-cell:hover:not(.ant-picker-cell-in-view):not(.custom-delivery-calendar .ant-picker-cell) {
+  background-color: transparent ;
   color: var(--color-theme);
+  .ant-picker-cell-inner {
+    background-color: var(--color-orange-6) !important;
+    color: var(--color-theme) !important;
+  }
 }
 .ant-picker-dropdown
   .ant-picker-date-panel
@@ -190,24 +206,27 @@ tr
 .ant-picker-input input {
   color: var(--color-neutral-1) !important;
 }
-.ant-picker-cell-inner {
-  display: flex !important;
-  align-items: center !important;
-  justify-content: center !important;
-  width: 32px !important;
-  height: 32px !important;
-  border-radius: 6px !important;
+.ant-picker-dropdown {
+  .ant-picker-cell-inner {
+    display: flex !important;
+    align-items: center !important;
+    justify-content: center !important;
+    border-radius: 6px !important;
+  }
+  
+  /* 其他弹窗样式保留 */
+  .ant-picker-cell-in-view .ant-picker-cell-inner {
+    color: var(--color-neutral-1);
+  }
 }
-:where(.css-dev-only-do-not-override-1p3hq3p).ant-picker-dropdown  div.ant-picker-cell-inner{
+.ant-picker-dropdown  div.ant-picker-cell-inner{
   color: var(--color-el-date-prev);
 }
 
-:where(.css-dev-only-do-not-override-1p3hq3p).ant-picker-dropdown .ant-picker-cell-in-view .ant-picker-cell-inner{
+.ant-picker-dropdown .ant-picker-cell-in-view .ant-picker-cell-inner{
   color: var(--color-neutral-1);
 }
-// :not(.ant-picker-cell-in-view).ant-picker-dropdown .ant-picker-cell .ant-picker-cell-inner {
-//   color: var(--color-el-date-prev);
-// }
+
 .ant-checkbox-checked .ant-checkbox-inner {
   background-color: var(--color-theme) !important;
   border-color: var(--color-theme) !important;
@@ -254,20 +273,39 @@ tr
   }
 }
 
-:where(.css-dev-only-do-not-override-1p3hq3p).ant-select-dropdown {
+div.ant-select-dropdown {
   background-color: var(--management-bg-color);
 }
-:where(.css-dev-only-do-not-override-1p3hq3p).ant-select:not(.ant-select-customize-input) div.ant-select-selector {
+.ant-select:not(.ant-select-customize-input) div.ant-select-selector {
   background-color: var(--management-bg-color);
+  border-color: var(--color-select-border);
+}
+
+.ant-select-focused.ant-select:not(.ant-select-disabled):not(.ant-select-customize-input):not(.ant-pagination-size-changer) div.ant-select-selector {
+  border-color: var(--color-theme) !important;
+}
+
+.ant-select-single.ant-select-open span.ant-select-selection-item {
+  color: var(--color-neutral-1);
 }
 
- :where(.css-dev-only-do-not-override-1p3hq3p).ant-select-single.ant-select-open .ant-select-selection-item {
-    color: var(--color-neutral-1);
- }
- :where(.css-dev-only-do-not-override-1p3hq3p).ant-select:not(.ant-select-customize-input) .ant-select-selector {
-  border-color: var(--color-select-border);
- }
 
  .rc-virtual-list-scrollbar-thumb {
   background-color: var(--color-scrollbar-thumb) !important;
+ }
+
+ .ant-select .ant-select-arrow {
+  color: var(--color-neutral-1);
+ }
+
+ .ant-picker,.ant-picker-range {
+  background-color: transparent !important;
+  border: 1px solid var(--color-select-border);
+  &.ant-picker-focused {
+    border-color: var(--color-theme) !important;
+    box-shadow: none;
+  }
+  .anticon-calendar {
+    color: var(--color-neutral-1) !important;
+  }
  }

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

@@ -1,9 +1,9 @@
 @font-face {
   font-family: "font_family"; /* Project id 4672385 */
-  src: url('iconfont.woff2?t=1764126605090') format('woff2'),
-       url('iconfont.woff?t=1764126605090') format('woff'),
-       url('iconfont.ttf?t=1764126605090') format('truetype'),
-       url('iconfont.svg?t=1764126605090#font_family') format('svg');
+  src: url('iconfont.woff2?t=1769408857052') format('woff2'),
+       url('iconfont.woff?t=1769408857052') format('woff'),
+       url('iconfont.ttf?t=1769408857052') format('truetype'),
+       url('iconfont.svg?t=1769408857052#font_family') format('svg');
 }
 
 .font_family {
@@ -14,6 +14,62 @@
   -moz-osx-font-smoothing: grayscale;
 }
 
+.icon-icon_search__user_b:before {
+  content: "\e752";
+}
+
+.icon-icon_convert_b:before {
+  content: "\e751";
+}
+
+.icon-icon_comment_b:before {
+  content: "\e74e";
+}
+
+.icon-icon_lost_b1:before {
+  content: "\e74f";
+}
+
+.icon-icon_won_b:before {
+  content: "\e750";
+}
+
+.icon-icon_lost_b:before {
+  content: "\e74d";
+}
+
+.icon-icon_page_b:before {
+  content: "\e74c";
+}
+
+.icon-icon_door_b1:before {
+  content: "\e74a";
+}
+
+.icon-icon_airport_b:before {
+  content: "\e74b";
+}
+
+.icon-icon_support_party_b:before {
+  content: "\e745";
+}
+
+.icon-icon_close_b:before {
+  content: "\e746";
+}
+
+.icon-icon_preview_b1:before {
+  content: "\e747";
+}
+
+.icon-icon_communic_ation_b1:before {
+  content: "\e748";
+}
+
+.icon-icon_detail_b:before {
+  content: "\e749";
+}
+
 .icon-icon_active:before {
   content: "\e744";
 }

File diff ditekan karena terlalu besar
+ 0 - 0
src/styles/icons/iconfont.js


File diff ditekan karena terlalu besar
+ 2 - 0
src/styles/icons/iconfont.svg


TEMPAT SAMPAH
src/styles/icons/iconfont.ttf


TEMPAT SAMPAH
src/styles/icons/iconfont.woff


TEMPAT SAMPAH
src/styles/icons/iconfont.woff2


+ 66 - 2
src/styles/theme.scss

@@ -268,8 +268,6 @@
   --color-upload-file-color: #b5b9bf;
   --color-upload-file-border-bg: #f5b279;
 
-  --color-personal-preference-bg: #f5f7fa;
-
   --color-password-card-first-bg: #ffe294;
   --color-password-card-second-bg: #f6f8fa;
   --color-password-expired-card-first-bg: #ef99a0;
@@ -364,6 +362,39 @@
   --color-attchment-summary-bg: #f9fafb;
 
   --color-el-date-prev: #bfc1c3;
+
+  --color-el-segmented-checked-bg: #fff;
+  --color-el-segmented-bg: #f1f5f9;
+
+  --color-calendar-cell-bg: #fff;
+
+  --color-delivery-calendar-booking-sign-bg:rgba(130, 0, 218, 0.1);
+  --color-delivery-calendar-booking-sign-border: rgba(130, 0, 218, 0.3);
+  --color-delivery-calendar-booking-label-text: #646a73;
+  --color-calendar-booking-tag-bg: rgba(130, 0, 218, 0.1);
+   --color-calendar-booking-tag-text: #8200da;
+  --color-calendar-booking-tag-label-text: rgba(130, 0, 218, 0.5);
+  --color-calendar-selected-cell-bg: #f4f4f5;
+  
+  --color-delivery-calendar-delivery-date-sign-bg:rgba(20, 71, 230, 0.1);
+  --color-delivery-calendar-delivery-date-sign-border: rgba(20, 71, 230, 0.3);
+  --color-delivery-calendar-delivery-date-label-text: #646a73;
+  --color-calendar-delivery-tag-bg: rgba(20, 71, 230, 0.1);
+  --color-calendar-delivery-tag-border: rgba(20, 71, 230, 0.3);
+  --color-calendar-delivery-tag-text: #1447e6;
+  --color-calendar-delivery-tag-label-text: rgba(20, 71, 230, 0.5);
+  
+  --color-delivery-calendar-ends-sign-bg:rgba(130, 0, 218, 0.1);
+  --color-delivery-calendar-ends-sign-border: rgba(193, 0, 8, 0.7);
+  --color-delivery-calendar-ends-label-text: #c10008;
+  --color-calendar-ending-tag-bg: rgba(193, 0, 8, 0.1);
+  --color-calendar-ending-tag-border: rgba(193, 0, 8, 0.7);
+  --color-calendar-ending-tag-text: #c10008;
+
+  --color-calendar-disabled-date-text: #b5b9bf;
+  --color-delivery-calendar-th-bg: #f6f8fa;
+
+  --color-delivery-date-picker-cell-bg: #b3e5d4;
 }
 
 :root.dark {
@@ -598,5 +629,38 @@
   --color-attchment-summary-bg: #2b2f36;
 
   --color-el-date-prev: #737980;
+
+  --color-el-segmented-checked-bg: #4b5563;
+  --color-el-segmented-bg: #374151;
+
+  --color-calendar-cell-bg: #30353c;
+  
+  --color-delivery-calendar-booking-sign-bg:rgba(130, 0, 218, 0.2);
+  --color-delivery-calendar-booking-sign-border: rgba(153, 1, 255, 0.7);
+  --color-delivery-calendar-booking-label-text: rgba(240, 241, 243, 0.70);
+  --color-calendar-booking-tag-bg: rgba(153, 1, 255, 0.2);
+  --color-calendar-booking-tag-text: #fff;
+  --color-calendar-booking-tag-label-text: rgba(255,255,255,0.5);
+  --color-calendar-selected-cell-bg: #3d4047;
+  
+  --color-delivery-calendar-delivery-date-sign-bg:rgba(31, 85, 255, 0.3);
+  --color-delivery-calendar-delivery-date-sign-border: rgba(31, 85, 255, 0.7);
+  --color-delivery-calendar-delivery-date-label-text: rgba(240, 241, 243, 0.70);
+  --color-calendar-delivery-tag-bg: rgba(31, 85, 255, 0.3);
+  --color-calendar-delivery-tag-border: rgba(31, 85, 255, 0.7);
+  --color-calendar-delivery-tag-text: #fff;
+  --color-calendar-delivery-tag-label-text: rgba(255,255,255,0.5);
+  
+  --color-delivery-calendar-ends-sign-bg:rgba(255, 18, 28, 0.2);
+  --color-delivery-calendar-ends-sign-border: rgba(255, 18, 28, 0.5);
+  --color-delivery-calendar-ends-label-text: rgba(255, 18, 28, 0.80);
+  --color-calendar-ending-tag-bg: rgba(255, 18, 28, 0.2);
+  --color-calendar-ending-tag-border: rgba(255, 18, 28, 0.5);
+  --color-calendar-ending-tag-text: #fff;
+
+  --color-calendar-disabled-date-text: rgba(240, 241, 243, 0.3);
+  --color-delivery-calendar-th-bg: #343a43;
+
+  --color-delivery-date-picker-cell-bg: #22584c;
 }
   

+ 32 - 16
src/utils/axios.ts

@@ -8,23 +8,39 @@ interface codeMessage {
   [key: number]: string
 }
 
-const CODE_MESSAGE: codeMessage = {
-  200: '服务器成功返回请求的数据。',
-  400: '发出的请求有错误,服务器没有进行新建或修改数据的操作。',
-  401: '用户没有权限(令牌、用户名、密码错误)。',
-  403: '用户得到授权,但是访问是被禁止的。',
-  404: '发出的请求针对的是不存在的记录,服务器没有进行操作。',
-  406: '请求的格式不可得。',
-  410: '请求的资源被永久删除,且不会再得到的。',
-  422: '当创建一个对象时,发生一个验证错误。',
-  456: 'refreshToken过期',
-  457: 'accessToken过期',
-  500: '服务器发生错误,请检查服务器。',
-  502: '网关错误。',
-  503: '服务不可用,服务器暂时过载或维护。',
-  504: '网关超时。'
-}
+// const CODE_MESSAGE: codeMessage = {
+//   200: '服务器成功返回请求的数据。',
+//   400: '发出的请求有错误,服务器没有进行新建或修改数据的操作。',
+//   401: '用户没有权限(令牌、用户名、密码错误)。',
+//   403: '用户得到授权,但是访问是被禁止的。',
+//   404: '发出的请求针对的是不存在的记录,服务器没有进行操作。',
+//   406: '请求的格式不可得。',
+//   410: '请求的资源被永久删除,且不会再得到的。',
+//   422: '当创建一个对象时,发生一个验证错误。',
+//   456: 'refreshToken过期',
+//   457: 'accessToken过期',
+//   500: '服务器发生错误,请检查服务器。',
+//   502: '网关错误。',
+//   503: '服务不可用,服务器暂时过载或维护。',
+//   504: '网关超时。'
+// }
 
+const CODE_MESSAGE: codeMessage = {
+  200: 'The server successfully returned the requested data.',
+  400: 'The request was invalid; the server did not create or modify any data.',
+  401: 'Unauthorized: invalid token, username, or password.',
+  403: 'Access is forbidden despite valid authentication.',
+  404: 'The requested resource does not exist; no action was taken by the server.',
+  406: 'The requested format is not available.',
+  410: 'The requested resource has been permanently deleted and will not be available again.',
+  422: 'A validation error occurred while creating an object.',
+  456: 'Refresh token expired.',
+  457: 'Access token expired.',
+  500: 'An internal server error occurred. Please check the server.',
+  502: 'Bad gateway.',
+  503: 'Service unavailable: the server is temporarily overloaded or under maintenance.',
+  504: 'Gateway timeout.'
+};
 class HttpAxios {
   instance: AxiosInstance
   timeout = 30000

+ 41 - 42
src/utils/table.ts

@@ -12,51 +12,50 @@ export const autoWidth = (tableData: VxeGridProps, grid: VxeGridInstance, custom
   const columnsWidth: { width: number; field: any }[] = []
   if (!columns || !data) return
   columns.forEach((column: any) => {
-    if (column?.field) {
-      let curStr = ''
-      let width = 0
-      const field = column.field
-      // 判断表头的宽度
-      if (column.title.length < 12) {
-        width = column.title.length * 11 + 40
-      } else if (column.title.length < 20) {
-        width = column.title.length * 10 + 30
+    if (!column.title) return
+    let curStr = ''
+    let width = 0
+    const field = column.field
+    // 判断表头的宽度
+    if (column.title.length < 12) {
+      width = column.title.length * 11 + 40
+    } else if (column.title.length < 20) {
+      width = column.title.length * 10 + 30
+    } else {
+      width = column.title.length * 7 + 40
+    }
+    const curData = data.length > 1000 ? data.slice(0, 1000) : data
+    // 判断表格内容的宽度
+    // 找到最长的字符串
+    curData.forEach((row: any) => {
+      curStr.length < row[field]?.toString().length && (curStr = row[field].toString())
+    })
+    // column.title.length > curStr.length && (curStr = column.title)
+    // 表头的宽度如果小于表格内容的宽度
+    if (width < curStr.length * 11) {
+      if (curStr.length > 20) {
+        width = curStr.length * 9 + 40
       } else {
-        width = column.title.length * 7 + 40
-      }
-      const curData = data.length > 1000 ? data.slice(0, 1000) : data
-      // 判断表格内容的宽度
-      // 找到最长的字符串
-      curData.forEach((row: any) => {
-        curStr.length < row[field]?.toString().length && (curStr = row[field].toString())
-      })
-      // column.title.length > curStr.length && (curStr = column.title)
-      // 表头的宽度如果小于表格内容的宽度
-      if (width < curStr.length * 11) {
-        if (curStr.length > 20) {
-          width = curStr.length * 9 + 40
-        } else {
-          width = curStr.length * 11 + 20
-        }
-      }
-      // 宽度不能小于100
-      // width < 100 && (width = 100)
-      // 最终宽度不能超过400
-      width > 400 && (width = 400)
-      // 如果字段是Mode,则固定宽度为80
-      if (field === 'Mode') {
-        width = 80
-      }
-      // 如果有自定义宽度,则使用自定义宽度
-      if (customizeWidth && customizeWidth[field]) {
-        width = customizeWidth[field]
+        width = curStr.length * 11 + 20
       }
-
-      columnsWidth.push({
-        width,
-        field: field
-      })
     }
+    // 宽度不能小于100
+    // width < 100 && (width = 100)
+    // 最终宽度不能超过400
+    width > 400 && (width = 400)
+    // 如果字段是Mode,则固定宽度为80
+    if (field === 'Mode') {
+      width = 80
+    }
+    // 如果有自定义宽度,则使用自定义宽度
+    if (customizeWidth && customizeWidth[field]) {
+      width = customizeWidth[field]
+    }
+
+    columnsWidth.push({
+      width,
+      field: field
+    })
   })
   columnsWidth.forEach((item) => {
     const curColumn: any = columns.find((column: any) => column.field === item.field)

+ 18 - 5
src/utils/tools.ts

@@ -1,21 +1,23 @@
-import moment from 'moment-timezone'
+import moment from 'moment-timezone';
+import 'moment-timezone/data/packed/latest.json';
 import { useUserStore } from '@/stores/modules/user'
 
+
 const userStore = useUserStore()
 const formatString = computed(() => {
   return userStore.dateFormat || 'MM/DD/YYYY'
 })
 
-export const formatTimezone = (time: string, timezone?: string, is12HourClock?: boolean) => {
+export const formatTimezone = (time: string, timezone?: string, is12HourClock?: boolean, originFormat?: string) => {
   if (!time) return '--'
   let formattedTime = ''
   if (time.length > 12) {
     if (is12HourClock) {
       // 如果是12小时制,使用12小时制格式化
-      formattedTime = moment(time).format(`${formatString.value} hh:mm A`)
+      formattedTime = moment(time, originFormat).format(`${formatString.value} hh:mm A`)
     } else {
       // 如果是24小时制,使用24小时制格式化
-      formattedTime = moment(time).format(`${formatString.value} HH:mm`)
+      formattedTime = moment(time, originFormat).format(`${formatString.value} HH:mm`)
     }
     if (!timezone) {
       return formattedTime
@@ -27,11 +29,22 @@ export const formatTimezone = (time: string, timezone?: string, is12HourClock?:
     utcOffset = `(UTC${timeZoneOffset.slice(0, 3)})`
     return `${formattedTime} ${utcOffset}`
   } else {
-    formattedTime = moment(time).format(formatString.value)
+    formattedTime = moment(time, originFormat).format(formatString.value)
     return formattedTime
   }
 }
 
+/**
+ * 将服务器时间转换为用户时区时间
+ * @param
+ * @returns
+ */
+export const formatTimezoneByUser = (time: string, timeFormate: string, showHour?: boolean) => {
+  if (!time) return '--'
+  let curFormatString = showHour ? formatString.value + ' HH:mm' : formatString.value
+  return moment.tz(time, timeFormate, 'America/Los_Angeles').local().format(curFormatString)
+}
+
 /**
  * 返回传入地区的UTC时区格式化
  * @param timezone

+ 144 - 393
src/views/Booking/src/BookingView.vue

@@ -1,5 +1,4 @@
 <script setup lang="ts">
-import emitter from '@/utils/bus'
 import FilterTags from '@/components/FliterTags'
 import TransportMode from '@/components/TransportMode'
 import BookingTable from './components/BookingTable'
@@ -9,362 +8,93 @@ import { ref, reactive } from 'vue'
 import { useCalculatingHeight } from '@/hooks/calculatingHeight'
 import { useUserStore } from '@/stores/modules/user'
 import dayjs from 'dayjs'
+import { useFiltersStore } from '@/stores/modules/filtersList'
 
 const userStore = useUserStore()
 const formatDate = userStore.dateFormat
-const valueFormatDate = 'MM/DD/YYYY'
+const filtersStore = useFiltersStore()
+const filtersList = computed(() => filtersStore.filtersList)
 
 const filterRef: Ref<HTMLElement | null> = ref(null)
-
 const containerHeight = useCalculatingHeight(document.documentElement, 246, [filterRef])
 
-const BookingSearch = ref()
-const tableLoadingTableData = ref(false)
-let searchTableQeury: any = {}
-const filterData = reactive({
-  filtersTagData: [] as Array<string>,
-  transportData: [] as Array<string>,
-  daterangeData: [] as Array<string>,
-  morefiltersData: [] as Array<string>
-})
+const tabList = ref([
+  {
+    name: 'All',
+    number: 0,
+    checked: true,
+    type: 'all'
+  },
+  {
+    name: 'Created',
+    number: 0,
+    checked: false,
+    type: 'created'
+  },
+  {
+    name: 'Confirmed',
+    number: 0,
+    checked: false,
+    type: 'confirmed'
+  },
+  {
+    name: 'Cancelled',
+    number: 0,
+    checked: false,
+    type: 'cancelled'
+  }
+])
 
-const tagsData: any = ref([])
-const handleClose = (tag: any) => {
-  emitter.emit('clearTag', tag)
-  tagsData.value.splice(tagsData.value.indexOf(tag), 1)
-  if (tag.includes('Transport')) {
-    delete searchTableQeury.transport_mode
-  } else if (tag.includes('ETD')) {
-    filterData.daterangeData.forEach((item: any, index: any) => {
-      if (item.includes('ETD')) {
-        filterData.daterangeData.splice(index, 1)
-      }
+const initPage = () => {
+  if (!filtersList.value || (filtersList.value && filtersList.value.length == 0)) {
+    filtersStore.updateFilter({
+      title: 'Transport Mode',
+      value: ['All'],
+      keyType: 'array',
+      key: 'transport_mode'
     })
-    delete searchTableQeury.f_etd_start
-    delete searchTableQeury.f_etd_end
-  } else if (tag.includes('ETA')) {
-    filterData.daterangeData.forEach((item: any, index: any) => {
-      if (item.includes('ETA')) {
-        filterData.daterangeData.splice(index, 1)
-      }
+    filtersStore.updateFilter({
+      title: 'ETD',
+      value: [
+        dayjs().subtract(2, 'month').startOf('month').format(formatDate),
+        dayjs().add(1, 'month').format(formatDate)
+      ],
+      keyType: 'dateRange',
+      key: ['f_etd_start', 'f_etd_end']
     })
-    delete searchTableQeury.m_eta_start
-    delete searchTableQeury.m_eta_end
-  } else if (tag.includes('Creation')) {
-    filterData.daterangeData.forEach((item: any, index: any) => {
-      if (item.includes('Creation')) {
-        filterData.daterangeData.splice(index, 1)
-      }
+    filtersStore.updateFilter({
+      title: 'Shipment Status',
+      value: ['All'],
+      keyType: 'array',
+      key: 'filterTag'
     })
-    delete searchTableQeury.created_time_start
-    delete searchTableQeury.created_time_end
-  } else if (tag.includes('Shippername')) {
-    delete searchTableQeury.shipper
-  } else if (tag.includes('Consigneename')) {
-    delete searchTableQeury.consignee
-  } else if (tag.includes('Origin Agent')) {
-    delete searchTableQeury.origin
-  } else if (tag.includes('Destination Agent')) {
-    delete searchTableQeury.agent
-  } else if (tag.includes('Sales')) {
-    delete searchTableQeury.sales_rep
-  } else if (tag.includes('Origin')) {
-    delete searchTableQeury.shipper_city
-  } else if (tag.includes('Destination')) {
-    delete searchTableQeury.consignee_city
-  } else if (tag.includes('Place of Receipt')) {
-    delete searchTableQeury['place_of_receipt/place_of_receipt_exp']
-  } else if (tag.includes('Port of Loading')) {
-    delete searchTableQeury['fport_of_loading_uncode/fport_of_loading_exp']
-  } else if (tag.includes('Place of delivery')) {
-    delete searchTableQeury['place_of_delivery/place_of_delivery_exp']
-  } else if (tag.includes('Vessel')) {
-    delete searchTableQeury['f_vessel/m_vessel']
-  } else if (tag.includes('Voyage')) {
-    delete searchTableQeury['f_voyage/m_voyage']
-  }
-  sessionStorage.setItem('searchTableQeury', JSON.stringify(searchTableQeury))
-  getbookingdata()
-}
-// 筛选框查询
-const FiltersSeach = (val: any, value: any) => {
-  searchTableQeury[val] = value
-  sessionStorage.setItem('searchTableQeury', JSON.stringify(searchTableQeury))
-  getbookingdata()
-}
-//TransportSearch
-const TransportSearch = (val: any) => {
-  filterData.transportData = []
-  if (val.data.length != 0) {
-    let str = `${val.title}:${val.data}`
-    filterData.transportData.push(str)
-  }
-  FiltersSeach('transport_mode', val.data)
-  renderTagsData()
-}
-// defaultTransport
-const defaultTransport = (val: any, value: any) => {
-  filterData.transportData = []
-  if (val.data.length != 0) {
-    let str = `${val.title}:${val.data}`
-    filterData.transportData.push(str)
-  }
-  if (sessionStorage.getItem('searchTableQeury') == null) {
-    searchTableQeury.transport_mode = val.data
-  } else {
-    searchTableQeury = value
   }
-  renderTagsData()
 }
-// defaultDate
-const defaultDate = (dateRangeData: any, data: any) => {
-  setFilterData(dateRangeData)
 
-  if (sessionStorage.getItem('searchTableQeury') == null) {
-    for (const key in dateRangeData) {
-      searchTableQeury.f_etd_start = dateRangeData[key].data[0]
-      searchTableQeury.f_etd_end = dateRangeData[key].data[1]
-    }
-  } else {
-    searchTableQeury = data
-    if (searchTableQeury._textSearch) {
-      BookingSearch.value = searchTableQeury._textSearch
-    }
-  }
-  getbookingdata()
-  renderTagsData()
-}
+initPage()
 
-const setFilterData = (dateRangeData: any) => {
-  filterData.daterangeData = []
-  for (const key in dateRangeData) {
-    const startEnd = dateRangeData[key].data[0] + ' To ' + dateRangeData[key].data[1]
-    let str = `${key}:${startEnd}`
-    filterData.daterangeData.push(str)
-  }
-}
-//DateRangeSearch
-const DateRangeSearch = (dateRangeData: any) => {
-  setFilterData(dateRangeData)
+const textSearch = ref()
+const tableLoadingTableData = ref(false)
 
-  if (Object.keys(dateRangeData).length == 0) {
-    delete searchTableQeury.f_etd_start
-    delete searchTableQeury.f_etd_end
-    delete searchTableQeury.m_eta_start
-    delete searchTableQeury.m_eta_end
-    delete searchTableQeury.created_time_start
-    delete searchTableQeury.created_time_end
-  }
-  const fieldList = [
-    {
-      title: 'ETD',
-      keys: ['f_etd_start', 'f_etd_end']
-    },
-    { title: 'ETA', keys: ['m_eta_start', 'm_eta_end'] },
-    { title: 'Creation Time', keys: ['created_time_start', 'created_time_end'] }
-  ]
-  fieldList.forEach((item) => {
-    if (!dateRangeData.hasOwnProperty(item.title)) {
-      // 删除不存在的字段
-      searchTableQeury[item.keys[0]] = undefined
-      searchTableQeury[item.keys[1]] = undefined
-    }
-  })
-  for (const key in dateRangeData) {
-    if (key == 'ETD') {
-      searchTableQeury.f_etd_start = dateRangeData[key].data[0]
-      searchTableQeury.f_etd_end = dateRangeData[key].data[1]
-    } else if (key == 'ETA') {
-      searchTableQeury.m_eta_start = dateRangeData[key].data[0]
-      searchTableQeury.m_eta_end = dateRangeData[key].data[1]
-    } else {
-      searchTableQeury.created_time_start = dateRangeData[key].data[0]
-      searchTableQeury.created_time_end = dateRangeData[key].data[1]
-    }
-  }
-  sessionStorage.setItem('searchTableQeury', JSON.stringify(searchTableQeury))
-  getbookingdata()
-  renderTagsData()
-}
-//MoreFiltersSearch
-const MoreFiltersSearch = (val: any, value: any) => {
-  filterData.morefiltersData = []
-  if (Object.keys(value).length == 0) {
-    delete searchTableQeury.shipper
-    delete searchTableQeury.consignee
-    delete searchTableQeury.origin
-    delete searchTableQeury.agent
-    delete searchTableQeury.sales_rep
-    delete searchTableQeury.shipper_city
-    delete searchTableQeury.consignee_city
-    delete searchTableQeury['place_of_receipt/place_of_receipt_exp']
-    delete searchTableQeury['fport_of_loading_uncode/fport_of_loading_exp']
-    delete searchTableQeury['place_of_delivery/place_of_delivery_exp']
-    delete searchTableQeury['f_vessel/m_vessel']
-    delete searchTableQeury['f_voyage/m_voyage']
-  }
-  for (const key in val) {
-    let str = `${key}:${val[key]}`
-    filterData.morefiltersData.push(str)
-    if (key == 'Shippername') {
-      searchTableQeury.shipper = value[key]
-    } else if (key == 'Consigneename') {
-      searchTableQeury.consignee = value[key]
-    } else if (key == 'Origin Agent') {
-      searchTableQeury.origin = value[key]
-    } else if (key == 'Destination Agent') {
-      searchTableQeury.agent = value[key]
-    } else if (key == 'Sales') {
-      searchTableQeury.sales_rep = value[key]
-    } else if (key == 'Origin') {
-      searchTableQeury.shipper_city = value[key]
-    } else if (key == 'Destination') {
-      searchTableQeury.consignee_city = value[key]
-    } else if (key == 'Place of Receipt') {
-      searchTableQeury['place_of_receipt/place_of_receipt_exp'] = value[key]
-    } else if (key == 'Port of Loading') {
-      searchTableQeury['fport_of_loading_uncode/fport_of_loading_exp'] = value[key]
-    } else if (key == 'Place of delivery') {
-      searchTableQeury['place_of_delivery/place_of_delivery_exp'] = value[key]
-    } else if (key == 'Vessel') {
-      searchTableQeury['f_vessel/m_vessel'] = value[key]
-    } else if (key == 'Voyage') {
-      searchTableQeury['f_voyage/m_voyage'] = value[key]
-    }
-  }
-  sessionStorage.setItem('searchTableQeury', JSON.stringify(searchTableQeury))
-  getbookingdata()
-  renderTagsData()
-}
-const defaultMorefilters = (val: any, value: any, data: any) => {
-  filterData.morefiltersData = []
-  for (const key in val) {
-    let str = `${key}:${val[key]}`
-    filterData.morefiltersData.push(str)
-    if (key == 'Shippername') {
-      searchTableQeury.shipper = value[key]
-    } else if (key == 'Consigneename') {
-      searchTableQeury.consignee = value[key]
-    } else if (key == 'Origin Agent') {
-      searchTableQeury.origin = value[key]
-    } else if (key == 'Destination Agent') {
-      searchTableQeury.agent = value[key]
-    } else if (key == 'Sales') {
-      searchTableQeury.sales_rep = value[key]
-    } else if (key == 'Origin') {
-      searchTableQeury.shipper_city = value[key]
-    } else if (key == 'Destination') {
-      searchTableQeury.consignee_city = value[key]
-    } else if (key == 'Place of Receipt') {
-      searchTableQeury['place_of_receipt/place_of_receipt_exp'] = value[key]
-    } else if (key == 'Port of Loading') {
-      searchTableQeury['fport_of_loading_uncode/fport_of_loading_exp'] = value[key]
-    } else if (key == 'Place of delivery') {
-      searchTableQeury['place_of_delivery/place_of_delivery_exp'] = value[key]
-    } else if (key == 'Vessel') {
-      searchTableQeury['f_vessel/m_vessel'] = value[key]
-    } else if (key == 'Voyage') {
-      searchTableQeury['f_voyage/m_voyage'] = value[key]
-    }
-  }
-  if (sessionStorage.getItem('searchTableQeury') != null) {
-    searchTableQeury = data
-  }
-  renderTagsData()
+const handleClose = (tagTitle: any) => {
+  filtersStore.deleteFilterByTitle(tagTitle)
+  getBookingData()
 }
+
 const clearfilters = () => {
-  BookingSearch.value = ''
-  filterData.filtersTagData = []
-  tagsData.value = []
-  let str = 'Shipment status: All'
-  filterData.filtersTagData.push(str)
-  filterData.transportData = []
-  filterData.daterangeData = []
-  filterData.morefiltersData = []
-  emitter.emit('clearTag', 'Shipment status')
-  emitter.emit('clearTag', 'Transport Mode')
-  emitter.emit('clearTag', 'ETD')
-  emitter.emit('clearTag', 'ETA')
-  emitter.emit('clearTag', 'Creation Time')
-  emitter.emit('clearTag', 'Shippername')
-  emitter.emit('clearTag', 'Consigneename')
-  emitter.emit('clearTag', 'Origin Agent')
-  emitter.emit('clearTag', 'Destination Agent')
-  emitter.emit('clearTag', 'Sales')
-  emitter.emit('clearTag', 'Origin')
-  emitter.emit('clearTag', 'Destination')
-  emitter.emit('clearTag', 'Place of Receipt')
-  emitter.emit('clearTag', 'Port of Loading')
-  emitter.emit('clearTag', 'Place of delivery')
-  emitter.emit('clearTag', 'Vessel')
-  emitter.emit('clearTag', 'Voyage')
-  searchTableQeury = {}
-  searchTableQeury.filterTag = ['All']
-  sessionStorage.setItem('searchTableQeury', JSON.stringify(searchTableQeury))
-  getbookingdata()
-  renderTagsData()
-}
-const renderTagsData = () => {
-  tagsData.value = []
-  if (filterData.transportData.length) {
-    tagsData.value.push(filterData.transportData[0])
-  }
-  if (filterData.daterangeData.length) {
-    filterData.daterangeData.forEach((item) => {
-      tagsData.value.push(item)
-    })
-  }
-  if (filterData.morefiltersData.length) {
-    filterData.morefiltersData.forEach((item) => {
-      tagsData.value.push(item)
-    })
-  }
-  if (filterData.filtersTagData.length) {
-    filterData.filtersTagData.forEach((item) => {
-      tagsData.value.push(item)
-    })
-  }
-}
-// 清除 Transport Tags
-const clearTransportTags = () => {
-  filterData.transportData = []
-}
-// 清除 Daterange Tags
-const clearDaterangeTags = () => {
-  filterData.daterangeData = []
-}
-// 清除 MoreFilters Tags
-const clearMoreFiltersTags = () => {
-  filterData.morefiltersData = []
+  filtersStore.clearFilters()
+  getBookingData()
 }
 
+const clearDaterangeTags = () => {}
+const clearMoreFiltersTags = () => {}
+
 const BookingTable_ref = ref()
-const TransportListItem = ref()
-interface ListItem {
-  name: string
-  number: number
-  type: string
-  checked: boolean
-}
-const TagsList = ref<ListItem[]>([])
-const filterTag = ref(['All'])
+const transportListItem = ref()
+
 const isShowAlertIcon = ref(false)
-const setSearchQeury = () => {}
-const getbookingdata = () => {
-  const dateRangeKeys = [
-    'f_etd_start',
-    'f_etd_end',
-    'm_eta_start',
-    'm_eta_end',
-    'created_time_start',
-    'created_time_end'
-  ]
-  const curRangeData: any = {}
-  for (const key of dateRangeKeys) {
-    if (searchTableQeury[key] !== undefined) {
-      curRangeData[key] = dayjs(searchTableQeury[key], formatDate).format(valueFormatDate)
-    }
-  }
+const getBookingData = () => {
+  const queryData = filtersStore.getQueryData
 
   tableLoadingTableData.value = true
   BookingTable_ref.value.getLoadingData(tableLoadingTableData.value)
@@ -374,30 +104,28 @@ const getbookingdata = () => {
       ps: BookingTable_ref.value.pageInfo.pageSize,
       rc: -1,
       other_filed: '',
-      ...searchTableQeury,
-      ...curRangeData
+      _textSearch: textSearch.value,
+      ...queryData
     })
     .then((res: any) => {
       if (res.code === 200) {
-        TransportListItem.value = res.data.TransportList
-        TagsList.value = res.data.tagsList
-        let taglist: any = []
-        if (Object.keys(filterData.filtersTagData).length == 0) {
-          TagsList.value.forEach((item: any) => {
-            if (item.checked == true) {
-              taglist.push(item.name)
-            }
-          })
-          let str = 'Shipment status: ' + taglist.toString()
-          filterData.filtersTagData.push(str)
-          filterData.filtersTagData.forEach((item) => {
-            tagsData.value.push(item)
-          })
-        }
+        transportListItem.value = res.data.TransportList
+
+        tabList.value = res.data.tagsList
+        const checkedTabNames = tabList.value
+          .filter((item) => item.checked)
+          .map((item) => item.name)
+        filtersStore.updateFilter({
+          title: 'Shipment Status',
+          value: checkedTabNames,
+          keyType: 'array',
+          key: 'filterTag'
+        })
+
         sessionStorage.setItem('BookingData', JSON.stringify(res.data))
-        BookingTable_ref.value.searchTableData(searchTableQeury)
+        BookingTable_ref.value.searchTableData()
         // 查询没结果的话显示icon
-        if (BookingSearch.value != '' && BookingSearch.value != undefined) {
+        if (textSearch.value != '' && textSearch.value != undefined) {
           if (res.data.searchData.length == 0) {
             isShowAlertIcon.value = true
           }
@@ -407,33 +135,63 @@ const getbookingdata = () => {
       }
     })
 }
-const changeTag = (val: any, boolean: any) => {
-  filterData.filtersTagData = []
-  searchTableQeury.filterTag = val
-  let str = 'Shipment status: ' + val
-  filterData.filtersTagData.push(str)
-  sessionStorage.setItem('searchTableQeury', JSON.stringify(searchTableQeury))
-  if (boolean) {
-    getbookingdata()
-  }
-  renderTagsData()
-  filterTag.value = val
+const tabChange = (changeTabList: any) => {
+  tabList.value = changeTabList
+  const checkedTabNames = tabList.value.filter((item) => item.checked).map((item) => item.name)
+  filtersStore.updateFilter({
+    title: 'Shipment Status',
+    value: checkedTabNames,
+    keyType: 'array',
+    key: 'filterTag'
+  })
+
+  getBookingData()
 }
 // 点击search按钮
 const SearchInput = () => {
-  searchTableQeury._textSearch = BookingSearch.value
-  sessionStorage.setItem('searchTableQeury', JSON.stringify(searchTableQeury))
-  getbookingdata()
+  filtersStore.updateFilter({
+    title: 'text search',
+    value: textSearch.value,
+    keyType: 'normal',
+    key: '_textSearch',
+    isHide: true
+  })
+  getBookingData()
 }
 
+onMounted(() => {
+  getBookingData()
+})
 import BookingGuide from './components/BookingGuide.vue'
 import { useGuideStore } from '@/stores/modules/guide'
+import { onBeforeRouteLeave } from 'vue-router'
 
 const guideStore = useGuideStore()
 const bookingGuideRef = ref()
 const handleGuide = () => {
   bookingGuideRef.value.startGuide() // 开始引导
 }
+
+onBeforeRouteLeave((route: any) => {
+  // guideStore.clearGuide()
+  const whiteList = ['Booking Detail', 'Tracking Detail']
+  if (!whiteList.includes(route?.name)) {
+    filtersStore.clearFilters()
+  }
+})
+
+const getTabsList = (list) => {
+  tabList.value.forEach((item) => {
+    const current = list.find((i) => i.name === item.name)
+    if (current) {
+      item.number = current.number
+    }
+  })
+}
+
+const handleSearch = () => {
+  getBookingData()
+}
 </script>
 
 <template>
@@ -445,12 +203,12 @@ const handleGuide = () => {
   <div class="display" ref="filterRef">
     <div class="filter-box">
       <div class="filters-container" id="booking-filters-container-guide">
-        <FilterTags :TagsListItem="TagsList" @changeTag="changeTag"></FilterTags>
+        <FilterTags :tagsList="tabList" @tabChange="tabChange"></FilterTags>
         <div class="heaer_top">
           <div class="search">
             <el-input
               placeholder="Enter Booking/HBL/PO/Carrier Booking No. "
-              v-model="BookingSearch"
+              v-model="textSearch"
               class="log_input"
               @keyup.enter="SearchInput"
             >
@@ -480,27 +238,18 @@ const handleGuide = () => {
             </el-input>
           </div>
           <TransportMode
-            :isShipment="false"
-            :TransportListItem="TransportListItem"
-            @TransportSearch="TransportSearch"
-            @defaultTransport="defaultTransport"
-            @clearTransportTags="clearTransportTags"
+            :transportListItem="transportListItem"
+            @transportSearch="getBookingData()"
           ></TransportMode>
           <DateRange
-            :isShipment="false"
-            @DateRangeSearch="DateRangeSearch"
+            @DateRangeSearch="getBookingData()"
             @clearDaterangeTags="clearDaterangeTags"
-            @defaultDate="defaultDate"
           ></DateRange>
         </div>
       </div>
       <MoreFilters
-        :isShipment="false"
-        :pageMode="'booking'"
-        :searchTableQeury="searchTableQeury"
-        @MoreFiltersSearch="MoreFiltersSearch"
+        @handleSearch="handleSearch"
         @clearMoreFiltersTags="clearMoreFiltersTags"
-        @defaultMorefilters="defaultMorefilters"
         :isShowMoreFiltersGuidePhoto="guideStore.booking.isShowMoreFiltersGuidePhoto"
       ></MoreFilters>
       <el-button class="el-button--dark" style="margin-left: 8px" @click="SearchInput"
@@ -508,34 +257,36 @@ const handleGuide = () => {
       >
     </div>
     <!-- 筛选项 -->
-    <div class="filtersTag" v-if="tagsData.length" id="booking-filter-tag-guide">
+    <div class="filtersTag" v-if="filtersStore.getTagsList.length" id="booking-filter-tag-guide">
       <el-tag
-        :key="tag"
         class="tag"
-        v-for="tag in tagsData"
-        :closable="!tag.includes('Shipment')"
+        v-for="tag in filtersStore.getTagsList"
+        :key="tag.title"
+        :closable="!tag.title.includes('Shipment')"
         :disable-transitions="false"
-        @close="handleClose(tag)"
+        @close="handleClose(tag.title)"
         color="#EFEFF0"
       >
         <el-tooltip
           class="box-item"
           effect="dark"
-          :content="tag"
+          :content="tag.value"
           placement="top"
-          v-if="tag.length > 39"
+          :popper-style="{ maxWidth: '500px', whiteSpace: 'normal', wordWrap: 'break-word' }"
+          v-if="tag.value.length > 39"
         >
-          {{ tag.length > 39 ? tag.substr(0, 39) + '...' : tag }}
+          {{ tag.value.length > 39 ? tag.value.slice(0, 39) + '...' : tag.value }}
         </el-tooltip>
-        <div v-else>{{ tag }}</div>
+        <div v-else>{{ tag.value }}</div>
       </el-tag>
       <div class="text_button" @click="clearfilters">Clear Filters</div>
     </div>
   </div>
   <BookingTable
+    :textSearch="textSearch"
     :height="containerHeight"
-    :tagsData="tagsData"
     ref="BookingTable_ref"
+    @getTabsList="getTabsList"
   ></BookingTable>
 </template>
 

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

@@ -2,7 +2,7 @@
 import '@wangeditor/editor/dist/css/style.css'
 import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
 import { i18nChangeLanguage, DomEditor } from '@wangeditor/editor'
-import { formatTimezone } from '@/utils/tools'
+import { formatTimezoneByUser } from '@/utils/tools'
 
 i18nChangeLanguage('en')
 
@@ -225,7 +225,9 @@ const sendEmail = () => {
             <div>{{ item.name?.slice(0, 1) }}</div>
           </div>
           <div class="name">{{ item.name }}</div>
-          <div class="date">{{ formatTimezone(item.creatTime) }}</div>
+          <div class="date">
+            {{ formatTimezoneByUser(item.creatTime, 'MM/DD/YYYY HH:mm:ss', true) }}
+          </div>
         </div>
         <div class="content">
           {{ item.content }}

+ 15 - 9
src/views/Booking/src/components/BookingTable/src/BookingTable.vue

@@ -10,6 +10,9 @@ import { transportationMode } from '@/components/transportationMode'
 import { useThemeStore } from '@/stores/modules/theme'
 import { useVisitedRowState } from '@/stores/modules/visitedRow'
 import { formatTimezone, formatNumber } from '@/utils/tools'
+import { useFiltersStore } from '@/stores/modules/filtersList'
+
+const filtersStore = useFiltersStore()
 
 const visitedRowState = useVisitedRowState()
 const themeStore = useThemeStore()
@@ -19,9 +22,9 @@ const props = defineProps({
     type: Number,
     default: 440
   },
-  tagsData: {
-    type: Array,
-    default: () => []
+  textSearch: {
+    type: String,
+    default: ''
   }
 })
 
@@ -115,7 +118,6 @@ assignPageInfo()
 
 const curTableData = ref([])
 const tempSearch = ref()
-const filterdataobj = ref()
 // 获得表格数据后赋值
 const assignTableData = (data: any) => {
   bookingTable.value.data = data.searchData || []
@@ -132,23 +134,28 @@ const assignTableData = (data: any) => {
     })
   }, 1000)
 }
+const emit = defineEmits(['getTabsList'])
 // 获取表格数据
 const getTableData = async (isPageChange?: boolean) => {
   const rc = isPageChange ? pageInfo.value.total : -1
+  const queryData = filtersStore.getQueryData
   // 保存页长以及当前页码
   sessionStorage.setItem('bookingTablePageInfo', JSON.stringify(pageInfo.value))
   tableLoadingTable.value = true
+
   await $api
     .getBookingTableData({
       cp: pageInfo.value.pageNo,
       ps: pageInfo.value.pageSize,
       rc,
       other_filed: '',
-      ...filterdataobj.value
+      _textSearch: props.textSearch,
+      ...queryData
     })
     .then((res: any) => {
       if (res.code === 200) {
         assignTableData(res.data)
+        emit('getTabsList', res.data.tagsList)
       }
     })
     .finally(() => {
@@ -161,8 +168,7 @@ const getTableData = async (isPageChange?: boolean) => {
     })
 }
 // 查询列表数据
-const searchTableData = (data: any) => {
-  filterdataobj.value = data
+const searchTableData = () => {
   if (sessionStorage.getItem('BookingData') != null) {
     const data = JSON.parse(sessionStorage.getItem('BookingData') as string) || {}
     assignTableData(data)
@@ -175,6 +181,8 @@ const searchTableData = (data: any) => {
         sessionStorage.removeItem('BookingData')
       })
     }, 100)
+  } else {
+    getTableData()
   }
 }
 
@@ -299,7 +307,6 @@ const handleDownload = () => {
     }
   })
   downloadDialogRef.value.openDialog(
-    props.tagsData,
     curSelectedColumns,
     selectedNumber.value || pageInfo.value.total
   )
@@ -331,7 +338,6 @@ const getExportTableData = (status: number) => {
   $api
     .getAllBookingTableData({
       selected_fields: column,
-      excel_filter_condition: props.tagsData.join(','),
       tmp_search: tempSearch.value
     })
     .then((res: any) => {

+ 18 - 4
src/views/Booking/src/components/BookingTable/src/components/DownloadDialog.vue

@@ -1,8 +1,11 @@
 <script setup lang="ts">
+import { useFiltersStore } from '@/stores/modules/filtersList'
+
+const filtersStore = useFiltersStore()
+
 const dialogVisible = ref(false)
 
-const openDialog = (tagsData: string[], selectedColumns: string[], slectedDataNumber: number) => {
-  listData.value = tagsData
+const openDialog = (selectedColumns: string[], slectedDataNumber: number) => {
   selectedDataNumber.value = slectedDataNumber
   columns.value = selectedColumns
   dialogVisible.value = true
@@ -12,7 +15,6 @@ const isShowSelectColumn = ref(false)
 
 const downloadFilter = ref(1)
 const selectedDataNumber = ref(0)
-const listData = ref()
 
 const columns = ref()
 
@@ -44,7 +46,19 @@ defineExpose({
           </div>
         </div>
         <div class="data-filter">
-          <div class="filter-item" v-for="item in listData" :key="item">{{ item }}</div>
+          <div class="filter-item" v-for="item in filtersStore.getTagsList" :key="item.title">
+            <el-tooltip
+              class="box-item"
+              effect="dark"
+              :content="item.value"
+              placement="top"
+              :popper-style="{ maxWidth: '500px', whiteSpace: 'normal', wordWrap: 'break-word' }"
+              v-if="item.value.length > 39"
+            >
+              {{ item.value.length > 39 ? item.value.slice(0, 39) + '...' : item.value }}
+            </el-tooltip>
+            <div v-else>{{ item.value }}</div>
+          </div>
         </div>
         <div class="download-filter">
           <el-radio-group v-model="downloadFilter">

File diff ditekan karena terlalu besar
+ 418 - 418
src/views/Dashboard/src/DashboardView.vue


+ 235 - 229
src/views/Dashboard/src/components/BarChart.vue

@@ -2,264 +2,269 @@
 <script lang="ts" setup>
 import * as echarts from 'echarts'
 import { useThemeStore } from '@/stores/modules/theme'
-import { onMounted, ref, reactive, watch, computed } from 'vue'
+import { onMounted, ref, reactive, watch, computed, onBeforeUnmount } from 'vue'
 import { formatNumber } from '@/utils/tools'
+
 const themeStore = useThemeStore()
+const barRef = ref<HTMLElement | null>(null)
+let chartInstance: echarts.ECharts | null = null
+
+// 定义 Props
 const props = defineProps({
-  BarData: Object,
-  barHeight: Object,
-  saveImageName: String
-})
-const bar_data = ref(props.BarData)
-const bar_ref = ref()
-watch(
-  () => bar_data.value,
-  (current) => {
-    bar_data.value = current
-    initOption.xAxis.data = barName.value
-    initOption.series = bar_series.value
-    initOption.legend.data = Name.value
-    initOption.yAxis.max = Max.value
-    initOption.yAxis.interval = interval.value
-    initOption.title.text = bar_title.value
-    nextTick(() => {
-      barChart.value.setOption(initOption)
-    })
+  BarData: {
+    type: Object,
+    default: () => ({})
+  },
+  barHeight: {
+    type: Object,
+    default: () => ({ height: '100%' })
   },
-  {
-    deep: true
+  saveImageName: {
+    type: String,
+    default: 'data'
   }
-)
-// 最大值
-const Max = computed(() => {
-  return bar_data.value?.Max
-})
-// 刻度
-const interval = computed(() => {
-  return bar_data.value?.interval
-})
-// x轴值
-const barName = computed(() => {
-  return bar_data.value?.barList
-})
-// title
-const bar_title = computed(() => {
-  return bar_data.value?.bar_title
-})
-// series
-const bar_series = computed(() => {
-  return bar_data.value?.barSeries
 })
-// legend标题
-const Name = computed(() => {
-  if (bar_data.value?.barSeries) {
-    return bar_data.value?.barSeries.map((item: any) => {
-      return item.name
-    })
-  } else {
-    return []
-  }
+
+// 计算属性 (直接从 props 获取,保持响应式)
+const barData = computed(() => props.BarData || {})
+const Max = computed(() => barData.value?.Max)
+const interval = computed(() => barData.value?.interval)
+const barName = computed(() => barData.value?.barList || [])
+const bar_title = computed(() => barData.value?.bar_title || '')
+const bar_series = computed(() => barData.value?.barSeries || [])
+const legendData = computed(() => {
+  return bar_series.value.map((item: any) => item.name)
 })
 
-// 数额
-const initOption = reactive({
-  //标题
-  title: {
-    text: bar_title.value || '', //主标题
-    left: 19,
-    top: 9.5,
-    textStyle: {
-      color: '#2B2F36',
-      fontWeight: '700',
-      fontFamily: 'Lato-Light',
-      fontSize: '14px'
-    }
-  },
-  // 间距
-  grid: {
-    top: '18%',
-    left: '1%',
-    right: '2%',
-    bottom: '5%',
-    containLabel: true // 距离包含坐标轴上的文字
-  },
-  // hover时的文字显示
-  tooltip: {
-    show: true,
-    trigger: 'axis',
-    axisPointer: {
-      type: 'shadow'
-    },
-    backgroundColor: '#2b2f36',
-    borderColor: '#2b2f36',
-    formatter: function (params: any) {
-      let str: any = ''
-      let allnum: any = 0
-      str += '<div style= ' + 'color:#FFF;font-family: Lato-Light>' + params[0].name + '</div>'
-      params.forEach((item: any) => {
-        allnum += item.value
-        allnum = Number(allnum.toFixed(2))
-        str +=
-          '<div style= ' +
-          'color:#FFF>' +
-          item.marker +
-          item.seriesName +
-          ': ' +
-          formatNumber(item.value) +
-          '</div>'
-      })
-      allnum = allnum.toFixed(2)
-      str +=
-        '<div style= ' +
-        'color:#FFF;font-family: Lato-Light>Total: ' +
-        formatNumber(allnum) +
-        '</div>'
-      return str
-    },
-    textStyle: {
-      color: '#FFF',
-      fontWeight: 700,
-      fontFamily: 'Lato-Light',
-      fontSize: '14px'
-    }
-  },
-  xAxis: {
-    type: 'category',
-    data: barName.value,
-    axisTick: {
-      show: false
-    },
-    axisLine: {
-      lineStyle: {
-        color: '#eaebed'
+// 构建完整的 Option 配置函数
+// 每次调用都返回最新的配置对象,避免手动修改 reactive 对象的麻烦
+const getOption = () => {
+  return {
+    title: {
+      text: bar_title.value,
+      left: 19,
+      top: 9.5,
+      textStyle: {
+        color: '#2B2F36',
+        fontWeight: '700',
+        fontFamily: 'Lato-Light',
+        fontSize: '14px'
       }
     },
-    axisLabel: {
-      interval: 0,
-      fontFamily: 'Lato-Light',
-      color: '#B5B9BF'
-    }
-  },
-  yAxis: {
-    type: 'value',
-    scale: true,
-    splitLine: {
-      show: true,
-      lineStyle: {
-        type: 'dashed',
-        color: '#3F434A'
-      }
+    grid: {
+      top: '18%',
+      left: '1%',
+      right: '2%',
+      bottom: '5%',
+      containLabel: true
     },
-    axisLine: {
+    tooltip: {
       show: true,
-      lineStyle: {
-        color: '#3F434A'
+      trigger: 'axis',
+      axisPointer: {
+        type: 'shadow'
+      },
+      backgroundColor: '#2b2f36',
+      borderColor: '#2b2f36',
+      formatter: (params: any) => {
+        if (!params || params.length === 0) return ''
+
+        let str = `<div style="color:#FFF;font-family: Lato-Light">${params[0].name}</div>`
+        let allnum = 0
+
+        params.forEach((item: any) => {
+          const val = Number(item.value) || 0
+          allnum += val
+          str += `<div style="color:#FFF">${item.marker}${item.seriesName}: ${formatNumber(val)}</div>`
+        })
+
+        allnum = Number(allnum.toFixed(2))
+        str += `<div style="color:#FFF;font-family: Lato-Light">Total: ${formatNumber(allnum)}</div>`
+        return str
+      },
+      textStyle: {
+        color: '#FFF',
+        fontWeight: 700,
+        fontFamily: 'Lato-Light',
+        fontSize: '14px'
       }
     },
-    axisLabel: {
-      fontFamily: 'Lato-Light',
-      color: '#B5B9BF',
-      formatter: function (value: any) {
-        return formatNumber(value, 0)
+    xAxis: {
+      type: 'category',
+      data: barName.value,
+      axisTick: { show: false },
+      axisLine: {
+        lineStyle: {
+          color: '#eaebed'
+        }
+      },
+      axisLabel: {
+        interval: 0,
+        fontFamily: 'Lato-Light',
+        color: '#B5B9BF'
       }
     },
-    min: 0, // 最小值
-    max: Max.value, // 最大值
-    interval: interval.value // 刻度
-  },
-  legend: {
-    data: Name.value,
-    top: '3%',
-    itemGap: 30,
-    left: '40%',
-    itemHeight: 8, //修改icon图形大小
-    itemWidth: 8, //修改icon图形大小
-    textStyle: {
-      fontSize: 12,
-      fontFamily: 'Lato-Light',
-      color: '#646A73'
-    }
-  },
-  toolbox: {
-    top: 6,
-    right: 8,
-    iconStyle: {
-      borderColor: '#2B2F36'
+    yAxis: {
+      type: 'value',
+      scale: true,
+      splitLine: {
+        show: true,
+        lineStyle: {
+          type: 'dashed',
+          color: '#3F434A'
+        }
+      },
+      axisLine: {
+        show: true,
+        lineStyle: {
+          color: '#3F434A'
+        }
+      },
+      axisLabel: {
+        fontFamily: 'Lato-Light',
+        color: '#B5B9BF',
+        formatter: (value: any) => formatNumber(value, 0)
+      },
+      min: 0,
+      max: Max.value,
+      interval: interval.value
+    },
+    legend: {
+      data: legendData.value,
+      top: '3%',
+      itemGap: 30,
+      left: '40%',
+      itemHeight: 8,
+      itemWidth: 8,
+      textStyle: {
+        fontSize: 12,
+        fontFamily: 'Lato-Light',
+        color: '#646A73'
+      }
     },
-    emphasis: {
+    toolbox: {
+      top: 6,
+      right: 8,
       iconStyle: {
-        borderColor: '#ff7500'
-      } // hover上去时的颜色
+        borderColor: '#2B2F36'
+      },
+      emphasis: {
+        iconStyle: {
+          borderColor: '#ff7500'
+        }
+      },
+      feature: {
+        saveAsImage: {
+          icon: 'path://M588.8 821.44H179.52a38.4 38.4 0 0 1-38.4-38.4v-105.088l202.432-115.584 112.96 64.576c7.872 4.48 17.6 4.48 25.472 0l400.768-228.8-0.32-0.512h0.64v-156.8a89.6 89.6 0 0 0-89.6-89.6H179.456a89.6 89.6 0 0 0-89.6 89.6v542.208a89.6 89.6 0 0 0 89.6 89.6H588.8v-51.2zM141.184 240.896a38.4 38.4 0 0 1 38.4-38.4h613.824a38.4 38.4 0 0 1 38.4 38.4v127.36L469.248 575.104 356.288 510.72a25.6 25.6 0 0 0-19.2-2.496l-6.144 2.56-189.76 108.288V240.896z m168.448 226.432c44.416 0 80.96-33.792 85.376-76.992l0.384-8.768c0-44.416-33.728-80.96-76.992-85.376l-8.768-0.448c-47.36 0-85.824 38.4-85.824 85.76l0.448 8.832c4.096 40.32 36.16 72.448 76.544 76.544l8.832 0.448z m0-51.2a34.624 34.624 0 1 1 0-69.312 34.624 34.624 0 0 1 0 69.312z m445.888 449.024a25.6 25.6 0 0 0 36.224-0.064l138.368-138.368-36.224-36.224-94.592 94.656V545.536h-51.2v239.616l-94.72-94.656-36.224 36.224 138.368 138.432z',
+          show: true,
+          name: bar_title.value || props.saveImageName || 'data',
+          type: 'png',
+          backgroundColor: '#fff'
+        }
+      },
+      showTitle: false
     },
-    feature: {
-      saveAsImage: {
-        show: true,
-        name: bar_title.value || props.saveImageName || 'data',
-        type: 'png',
-        backgroundColor: '#fff'
+    series: bar_series.value
+  }
+}
+
+// 初始化图表
+const initChart = () => {
+  if (!barRef.value) return
+
+  // 1. 如果实例已存在,先销毁(防止重复初始化和内存泄漏)
+  if (chartInstance) {
+    chartInstance.dispose()
+  }
+
+  // 2. 创建新实例
+  chartInstance = echarts.init(barRef.value)
+
+  // 3. 设置配置
+  chartInstance.setOption(getOption())
+
+  // 4. 绑定点击事件 (直接使用当前实例)
+  chartInstance.on('click', (params: any) => {
+    paramsdata.value.name = params.name
+    paramsdata.value.type = params.seriesName
+    emits('ClickParams')
+  })
+
+  // 5. 处理 Resize (使用 once 或确保只绑定一次逻辑,这里简单处理为每次 init 都重新绑定前需解绑,但 echarts 内部通常处理较好)
+  // 更好的做法是将 resize 逻辑提取到外部或使用 lodash debounce,这里保持简单但确保逻辑正确
+}
+
+// 监听数据变化
+watch(
+  () => props.BarData,
+  () => {
+    if (chartInstance) {
+      chartInstance.setOption(getOption(), true) // true = notMerge, 强制重置系列数据,防止旧数据残留
+    }
+  },
+  { deep: true }
+)
+
+// 监听主题变化
+watch(
+  () => themeStore.theme,
+  (newVal) => {
+    if (!chartInstance) return
+
+    // 动态修改颜色配置
+    const isDark = newVal === 'dark'
+    const textColor = isDark ? '#f0f1f3' : '#2B2F36'
+    const axisColor = isDark ? '#8d9095' : '#eaebed'
+    const splitLineColor = isDark ? '#8d9095' : '#eaebed' // 注意原代码 dark 模式下 splitLine 也是 #8d9095
+    const toolBorderColor = isDark ? '#f0f1f3' : '#2B2F36'
+    const saveBgColor = isDark ? '#3F434A' : '#fff'
+
+    // 使用 setOption 仅更新变化的部分
+    chartInstance.setOption({
+      title: { textStyle: { color: textColor } },
+      xAxis: { axisLine: { lineStyle: { color: axisColor } } },
+      yAxis: {
+        axisLine: { lineStyle: { color: axisColor } },
+        splitLine: { lineStyle: { color: splitLineColor } }
+      },
+      toolbox: {
+        iconStyle: { borderColor: toolBorderColor },
+        feature: {
+          saveAsImage: { backgroundColor: saveBgColor }
+        }
       }
-    },
-    showTitle: false
+    })
   },
-  series: bar_series.value
-})
+  { immediate: true }
+)
+
+// 监听窗口大小变化
+const handleResize = () => {
+  if (chartInstance) {
+    chartInstance.resize()
+  }
+}
+
 onMounted(() => {
   initChart()
-  clickParams()
+  window.addEventListener('resize', handleResize)
+})
 
-  watch(
-    () => themeStore.theme,
-    (newVal) => {
-      if (newVal === 'dark') {
-        initOption.title.textStyle.color = '#f0f1f3'
-        initOption.xAxis.axisLine.lineStyle.color = '#8d9095'
-        initOption.yAxis.axisLine.lineStyle.color = '#8d9095'
-        initOption.yAxis.splitLine.lineStyle.color = '#8d9095'
-        initOption.toolbox.iconStyle.borderColor = '#f0f1f3'
-        initOption.toolbox.feature.saveAsImage.backgroundColor = '#3F434A'
-        nextTick(() => {
-          barChart.value.setOption(initOption)
-        })
-      } else {
-        initOption.title.textStyle.color = '#2B2F36'
-        initOption.xAxis.axisLine.lineStyle.color = '#eaebed'
-        initOption.yAxis.axisLine.lineStyle.color = '#eaebed'
-        initOption.yAxis.splitLine.lineStyle.color = '#eaebed'
-        initOption.toolbox.iconStyle.borderColor = '#2B2F36'
-        initOption.toolbox.feature.saveAsImage.backgroundColor = '#fff'
-        nextTick(() => {
-          barChart.value.setOption(initOption)
-        })
-      }
-    },
-    {
-      immediate: true,
-      deep: true
-    }
-  )
+onBeforeUnmount(() => {
+  // 清理资源
+  window.removeEventListener('resize', handleResize)
+  if (chartInstance) {
+    chartInstance.dispose()
+    chartInstance = null
+  }
 })
 
+// 暴露数据给父组件
 const emits = defineEmits(['ClickParams'])
 const paramsdata = ref({
   name: '',
   type: ''
 })
-const clickParams = () => {
-  // 监听点击事件
-  barChart.value.on('click', function (params) {
-    paramsdata.value.name = params.name
-    paramsdata.value.type = params.seriesName
-    emits('ClickParams')
-  })
-}
-const barChart = ref()
-const initChart = () => {
-  barChart.value = echarts.init(bar_ref.value)
-  //图表响应式
-  window.addEventListener('resize', () => {
-    barChart.value.resize()
-  })
-}
 
 defineExpose({
   paramsdata
@@ -268,7 +273,7 @@ defineExpose({
 
 <template>
   <div class="com-container">
-    <div ref="bar_ref" id="bar_chart" :style="props.barHeight"></div>
+    <div ref="barRef" class="bar-chart" :style="barHeight"></div>
   </div>
 </template>
 
@@ -279,7 +284,8 @@ defineExpose({
   overflow: hidden;
   position: relative;
 }
-#bar_chart {
+.bar-chart {
   width: 100%;
+  height: 100%;
 }
 </style>

+ 196 - 0
src/views/Dashboard/src/components/CustomerFilter.vue

@@ -0,0 +1,196 @@
+<script setup lang="ts">
+import { cloneDeep, debounce } from 'lodash'
+import axios from 'axios'
+
+const props = defineProps({
+  data: {
+    type: Object as () => {
+      customerCode: string[]
+      customerType: string[]
+    }
+  },
+  default: () => {
+    return {
+      customerCode: [],
+      customerType: []
+    }
+  }
+})
+
+const customerInfo = ref({
+  customerCode: [] as string[],
+  customerType: [] as string[]
+})
+
+watch(
+  () => props.data,
+  (newValue) => {
+    customerInfo.value = cloneDeep(newValue) || {
+      customerCode: [],
+      customerType: []
+    }
+  },
+  {
+    deep: true,
+    immediate: true
+  }
+)
+const emit = defineEmits(['changeCustomerData', 'handleCumstomerSearch'])
+const changeData = (val: string[], type: string) => {
+  // 同步选中状态
+  emit('changeCustomerData', val, type)
+}
+
+interface ListItem {
+  label: string
+  id: string
+}
+
+const options = ref<ListItem[]>([])
+const loading = ref(false)
+const currentController = ref<AbortController | null>(null)
+
+const handleVisibleChange = (visible) => {
+  !visible && (options.value = [])
+}
+const remoteMethod = (query: string) => {
+  currentController.value?.abort()
+
+  const newController = new AbortController()
+  currentController.value = newController
+  loading.value = true
+
+  $api
+    .getSpecificRolesPartyID({ term: query }, { signal: newController.signal })
+    .then((res) => {
+      if (!newController.signal.aborted && res.code === 200) {
+        options.value = (res.data || []).map((item) => ({
+          id: item.id,
+          label: item.label
+        }))
+      }
+    })
+    .catch((err) => {
+      options.value = []
+      if (!axios.isCancel(err) && !newController.signal.aborted) {
+        ElMessage.error('Failed to load options')
+      }
+    })
+    .finally(() => {
+      // 仅当这是最新请求时,才关闭 loading
+      if (currentController.value === newController) {
+        loading.value = false
+      }
+    })
+}
+
+// 防抖版本(可选)
+const debouncedRemoteMethod = debounce(remoteMethod, 200)
+
+onUnmounted(() => {
+  currentController.value?.abort()
+})
+
+const typeOptions = ref([
+  {
+    label: 'Shipper',
+    value: 'shipper_id'
+  },
+  {
+    label: 'Consignee',
+    value: 'consignee_id'
+  },
+  {
+    label: 'Controlling Customer',
+    value: 'customer_code'
+  },
+  {
+    label: 'Bill to',
+    value: 'billto_id'
+  },
+  {
+    label: 'Notify Party',
+    value: 'notify_party_id'
+  }
+])
+
+const handleSearch = () => {
+  emit('handleCumstomerSearch')
+}
+</script>
+
+<template>
+  <div class="customer-filter">
+    <el-select
+      :model-value="customerInfo.customerCode"
+      multiple
+      filterable
+      reserve-keyword
+      placeholder="Search by Customer code, Customer name"
+      :loading="loading"
+      style="width: 400px"
+      collapse-tags
+      :max-collapse-tags="2"
+      collapse-tags-tooltip
+      popper-class="part-id-select-popper"
+      :filter-method="debouncedRemoteMethod"
+      @change="changeData($event, 'customerCode')"
+      @visible-change="handleVisibleChange"
+    >
+      <el-option
+        v-for="item in options"
+        :key="item.id + item.label"
+        :label="item.label"
+        :value="item.label"
+      >
+        <div class="select-option">
+          <el-checkbox
+            :model-value="customerInfo.customerCode.includes(item.label)"
+            style="flex: 1"
+          >
+            <span style="display: inline-block; width: 220px">{{ item.label }}</span>
+            <span class="text-ellipsis" style="flex: 1; width: 200px">{{ item.id }}</span>
+          </el-checkbox>
+        </div>
+      </el-option>
+    </el-select>
+    <el-select
+      placeholder="Customer Type"
+      multiple
+      :max-collapse-tags="1"
+      collapse-tags
+      collapse-tags-tooltip
+      :model-value="customerInfo.customerType"
+      @change="changeData($event, 'customerType')"
+      style="width: 240px; margin-left: 8px"
+    >
+      <el-option
+        v-for="item in typeOptions"
+        :key="item.value"
+        :label="item.label"
+        :value="item.value"
+      >
+        <div class="select-option">
+          <el-checkbox
+            :model-value="customerInfo.customerType.includes(item.value)"
+            style="flex: 1"
+          >
+            <span style="display: inline-block; width: 220px">{{ item.label }}</span>
+          </el-checkbox>
+        </div>
+      </el-option>
+    </el-select>
+    <el-button class="el-button--default" @click="handleSearch">Search</el-button>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.customer-filter {
+  display: flex;
+  align-items: center;
+  height: 48px;
+  padding-left: 24px;
+  border: 1px solid var(--color-border);
+  margin-bottom: 8px;
+}
+</style>

+ 92 - 90
src/views/Dashboard/src/components/DashFiters.vue

@@ -1,6 +1,5 @@
 <script setup lang="ts">
 import { ref, watch, onMounted, computed } from 'vue'
-import moment from 'moment'
 import dayjs from 'dayjs'
 import { formatTimezone } from '@/utils/tools'
 
@@ -8,7 +7,7 @@ const props = defineProps({
   defaultData: {
     type: Object
   },
-  radioisDisabled: {
+  isPending: {
     type: Boolean,
     default: false
   },
@@ -31,29 +30,23 @@ const props = defineProps({
   isShowTransportModeGuide: {
     type: Boolean,
     default: false
+  },
+  threeMonthsInterval: {
+    type: Boolean,
+    default: false
   }
 })
 
-const defaultfiltersData = ref(props.defaultData)
-watch(
-  () => props.defaultData,
-  (current) => {
-    defaultfiltersData.value = current
-  }
-)
-const isDisabled = ref(false)
 const checkboxGroup1 = ref(['All'])
 const CheckboxGroup2 = ref('ETD')
-const CheckboxGroup3 = ref('invoice Issue Date')
+const CheckboxGroup3 = ref('Invoice Issue Date')
 const filters_visible = ref(false)
 const shipper = ref(['All', 'Air', 'Sea', 'Road', 'Rail'])
 const shipper_two = ref(['ETD', 'ETA'])
-const shipper_three = ref(['invoice Issue Date'])
-const DashDate = ref()
-DashDate.value = []
+const shipper_three = ref(['Invoice Issue Date'])
+const DashDate = ref([])
 const startDate = ref()
 const EndDate = ref()
-let dashboardObj: any = {}
 
 const checkboxDisabled = computed(() => {
   if (props.isContainer && !props.isETDToETA) {
@@ -62,42 +55,86 @@ const checkboxDisabled = computed(() => {
     return false
   }
 })
-onMounted(() => {
-  getdefaultdata()
-})
-const getdefaultdata = () => {
-  checkboxGroup1.value = defaultfiltersData.value?.transportation
-  CheckboxGroup2.value = defaultfiltersData.value?.date_type
-  if (defaultfiltersData.value?.date_start == '') {
-    DashDate.value = []
-    isDisabled.value = true
+// onMounted(() => {
+//   getdefaultdata()
+// })
+
+const emit = defineEmits(['FilterSearch'])
+const timeRange = ref([])
+const DateChange = (value: any) => {
+  timeRange.value = value
+}
+const DateRangeSearch = () => {
+  let date = { date_start: '', date_end: '' }
+  if (!props.isPending) {
+    date = props.isContainer
+      ? {
+          date_start: dayjs(startDate.value).format('MM/YYYY'),
+          date_end: dayjs(EndDate.value).format('MM/YYYY')
+        }
+      : {
+          date_start: dayjs(timeRange.value[0]).format('MM/DD/YYYY'),
+          date_end: dayjs(timeRange.value[1]).format('MM/DD/YYYY')
+        }
+  }
+  const data = {
+    transportation: checkboxGroup1.value,
+    date_type: CheckboxGroup2.value,
+    date_start: date.date_start,
+    date_end: date.date_end
+  }
+  emit('FilterSearch', data)
+  filters_visible.value = false
+}
+const getdefaultdata = (data: any, isReset: boolean = false) => {
+  checkboxGroup1.value = data?.transportation
+  CheckboxGroup2.value = data?.date_type
+
+  if (props.isContainer) {
+    // 容器模式:固定12个月范围(含当前月)
+    startDate.value = dayjs().subtract(12, 'month').format('YYYY-MM')
+    EndDate.value = dayjs().format('YYYY-MM')
   } else {
-    if (props.isContainer) {
-      startDate.value = defaultfiltersData.value?.date_start_two
-      EndDate.value = defaultfiltersData.value?.date_end_two
-    } else {
-      DashDate.value = [
-        dayjs(defaultfiltersData.value?.date_start_two),
-        dayjs(defaultfiltersData.value?.date_end_two)
-      ]
-    }
-    if (props.isContainer) {
-      dashboardObj.date_start = defaultfiltersData.value?.date_start
-      dashboardObj.date_start_two = defaultfiltersData.value?.date_start_two
-      dashboardObj.date_end = defaultfiltersData.value?.date_end
-      dashboardObj.date_end_two = defaultfiltersData.value?.date_end_two
+    if (props.isPending) {
+      DashDate.value = ['', '']
     } else {
-      dashboardObj.date_start = dayjs(DashDate.value[0]).format('MM/DD/YYYY')
-      dashboardObj.date_start_two = dayjs(DashDate.value[0]).format('YYYY-MM-DD')
-      dashboardObj.date_end = dayjs(DashDate.value[1]).format('MM/DD/YYYY')
-      dashboardObj.date_end_two = dayjs(DashDate.value[1]).format('YYYY-MM-DD')
+      const monthsInterval = props.threeMonthsInterval ? 3 : 12
+
+      let start = dayjs().subtract(monthsInterval, 'month').startOf('month')
+      let end = dayjs()
+
+      // isRecent 模式下,结束时间延后一个月
+      if (props.isRecent) {
+        end = dayjs().add(1, 'month')
+      }
+
+      DashDate.value = [start, end]
     }
+    timeRange.value = DashDate.value
+  }
+  if (!isReset) {
+    DateRangeSearch()
   }
-  dashboardObj.transportation = checkboxGroup1.value
-  dashboardObj.date_type = CheckboxGroup2.value
 }
+let unwatch // 先声明
+
+unwatch = watch(
+  () => props.defaultData,
+  (value) => {
+    if (value && Object.keys(value).length > 0) {
+      nextTick(() => {
+        getdefaultdata(value)
+        unwatch() // ✅ 现在可以安全调用
+      })
+    }
+  },
+  {
+    immediate: true,
+    deep: true
+  }
+)
 const changeCheckboxGroup1 = (val: any) => {
-  if (val.length == 3) {
+  if (val.length == 4) {
     checkboxGroup1.value = ['All']
   }
   if (val.length == 0) {
@@ -109,18 +146,8 @@ const changeCheckboxGroup1 = (val: any) => {
       checkboxGroup1.value.splice(val.indexOf('All'), 1)
     }
   }
-  dashboardObj.transportation = checkboxGroup1.value
-}
-const changeCheckboxGroup2 = (val: any) => {
-  dashboardObj.date_type = val
-}
-const emit = defineEmits(['FilterSearch'])
-const DateChange = (value: any) => {
-  dashboardObj.date_start = value[0]
-  dashboardObj.date_start_two = dayjs(value[0]).format('YYYY-MM-DD')
-  dashboardObj.date_end = value[1]
-  dashboardObj.date_end_two = dayjs(value[1]).format('YYYY-MM-DD')
 }
+
 const StartChange = (val: any) => {
   if (!props.isETDToETA) {
     const nextMonth = new Date(val)
@@ -129,14 +156,6 @@ const StartChange = (val: any) => {
     let current = currentYear + '-' + currentMonth
     EndDate.value = current
   }
-  const date1 = moment(String(val)).format('MM/YYYY')
-  const date1_two = moment(String(val)).format('MM-YYYY')
-  const date2 = moment(EndDate.value).format('MM/YYYY')
-  const date2_two = moment(EndDate.value).format('MM-YYYY')
-  dashboardObj.date_start = date1
-  dashboardObj.date_start_two = date1_two
-  dashboardObj.date_end = date2
-  dashboardObj.date_end_two = date2_two
 }
 const EndChange = (val: any) => {
   if (!props.isETDToETA) {
@@ -146,31 +165,13 @@ const EndChange = (val: any) => {
     let current = currentYear + '-' + currentMonth
     startDate.value = current
   }
-  const date1 = moment(startDate.value).format('MM/YYYY')
-  const date1_two = moment(startDate.value).format('MM-YYYY')
-  const date2 = moment(String(val)).format('MM/YYYY')
-  const date2_two = moment(String(val)).format('MM-YYYY')
-  dashboardObj.date_start = date1
-  dashboardObj.date_start_two = date1_two
-  dashboardObj.date_end = date2
-  dashboardObj.date_end_two = date2_two
 }
 // 清除
 const clearrest = () => {
-  getdefaultdata()
-}
-// 查询
-const DateRangeSearch = () => {
-  if (props.isRecent) {
-    emit('FilterSearch', false, dashboardObj)
-  } else {
-    emit('FilterSearch', dashboardObj)
-  }
-  filters_visible.value = false
+  getdefaultdata({}, true)
 }
 
 import { useThemeStore } from '@/stores/modules/theme'
-
 import { useGuideStore } from '@/stores/modules/guide'
 import transportModeLight from '@/views/Dashboard/src/guideImage/transport-mode.png'
 import transportModeDark from '@/views/Dashboard/src/guideImage/dark-transport-mode.png'
@@ -213,7 +214,12 @@ const guideStore = useGuideStore()
           size="large"
           :disabled="checkboxDisabled"
         >
-          <el-checkbox-button class="filter_button" v-for="item in shipper" :key="item" :value="item">
+          <el-checkbox-button
+            class="filter_button"
+            v-for="item in shipper"
+            :key="item"
+            :value="item"
+          >
             {{ item }}
           </el-checkbox-button>
         </el-checkbox-group>
@@ -232,11 +238,7 @@ const guideStore = useGuideStore()
           </el-radio-group>
         </div>
         <div class="filter_filter" v-else>
-          <el-radio-group
-            v-model="CheckboxGroup2"
-            @change="changeCheckboxGroup2"
-            :disabled="props.radioisDisabled"
-          >
+          <el-radio-group v-model="CheckboxGroup2" :disabled="props.isPending">
             <el-radio-button v-for="item in shipper_two" :key="item" :value="item" :label="item">
             </el-radio-button>
           </el-radio-group>
@@ -275,7 +277,7 @@ const guideStore = useGuideStore()
             v-else
             @DateChange="DateChange"
             :Date="DashDate"
-            :isDisabled="isDisabled"
+            :isDisabled="isPending"
           ></QuickCalendarDate>
         </div>
       </div>

+ 7 - 1
src/views/Dashboard/src/components/PieChart.vue

@@ -125,7 +125,13 @@ const initOption: any = reactive({
     show: true, // 显示工具箱
     feature: {
       // restore: { show: true },
-      saveAsImage: { show: true, name: downloadName.value, type: 'png', backgroundColor: '#fff' }
+      saveAsImage: {
+        icon: 'path://M588.8 821.44H179.52a38.4 38.4 0 0 1-38.4-38.4v-105.088l202.432-115.584 112.96 64.576c7.872 4.48 17.6 4.48 25.472 0l400.768-228.8-0.32-0.512h0.64v-156.8a89.6 89.6 0 0 0-89.6-89.6H179.456a89.6 89.6 0 0 0-89.6 89.6v542.208a89.6 89.6 0 0 0 89.6 89.6H588.8v-51.2zM141.184 240.896a38.4 38.4 0 0 1 38.4-38.4h613.824a38.4 38.4 0 0 1 38.4 38.4v127.36L469.248 575.104 356.288 510.72a25.6 25.6 0 0 0-19.2-2.496l-6.144 2.56-189.76 108.288V240.896z m168.448 226.432c44.416 0 80.96-33.792 85.376-76.992l0.384-8.768c0-44.416-33.728-80.96-76.992-85.376l-8.768-0.448c-47.36 0-85.824 38.4-85.824 85.76l0.448 8.832c4.096 40.32 36.16 72.448 76.544 76.544l8.832 0.448z m0-51.2a34.624 34.624 0 1 1 0-69.312 34.624 34.624 0 0 1 0 69.312z m445.888 449.024a25.6 25.6 0 0 0 36.224-0.064l138.368-138.368-36.224-36.224-94.592 94.656V545.536h-51.2v239.616l-94.72-94.656-36.224 36.224 138.368 138.432z',
+        show: true,
+        name: downloadName.value,
+        type: 'png',
+        backgroundColor: '#fff'
+      }
     },
     showTitle: false
   },

+ 1 - 1
src/views/Dashboard/src/components/RecentStatus.vue

@@ -84,7 +84,7 @@ const SubscribeShipments = (val: any) => {
       </div>
       <div class="recent-header-right">
         <el-button
-          class="recent_button"
+          class="el-button--default"
           @click="SubscribeShipments(item)"
           :class="item.is_subscribe ? 'IsSubscribe' : ''"
         >

+ 124 - 12
src/views/Dashboard/src/components/RevenueChart.vue

@@ -3,7 +3,6 @@
 import * as echarts from 'echarts'
 import { useThemeStore } from '@/stores/modules/theme'
 import { onMounted, ref, reactive, watch, computed } from 'vue'
-import updateIcon from '../image/xiazai.png'
 import * as XLSX from 'xlsx'
 import { formatNumber } from '@/utils/tools'
 
@@ -82,6 +81,84 @@ const isShowTooltips = computed(() => {
   return bar_data.value?.isShowTooltips
 })
 
+const downloadName = computed(() => {
+  return bar_data.value?.download_name
+})
+const exportData = ({ filename }: any) => {
+  $api
+    .RevenueDownload({
+      date_start: props.RevenueStartDate,
+      date_end: props.RevenueEndDate
+    })
+    .then((res: any) => {
+      if (res.code === 200) {
+        let header: any = []
+        let data: any = []
+        let sheetname: any = []
+        let filterA = res.data.r2_cloumn.map((el: any) => {
+          return el.dataIndex
+        })
+        let titleA = res.data.r2_cloumn.map((el: any) => {
+          return el.title
+        })
+        let filterB = res.data.r3_cloumn.map((el: any) => {
+          return el.dataIndex
+        })
+        let titleB = res.data.r3_cloumn.map((el: any) => {
+          return el.title
+        })
+        let result: any = [
+          {
+            tHeader: titleA,
+            filterVal: filterA,
+            tableDatas: res.data.r2,
+            sheetName: res.data.r2_title
+          },
+          {
+            tHeader: titleB,
+            filterVal: filterB,
+            tableDatas: res.data.r3,
+            sheetName: res.data.r3_title
+          }
+        ]
+        let formatJson = (filterVal: any, jsonData: any = []) => {
+          return jsonData?.map((v: any) => filterVal.map((j: any) => v[j]))
+        }
+        for (var i in result) {
+          header.push(result[i].tHeader)
+          sheetname.push(result[i].sheetName)
+          data.push(formatJson(result[i].filterVal, result[i].tableDatas))
+        }
+        // 将表头插入数据数组中
+        for (let i = 0; i < header.length; i++) {
+          data[i]?.unshift(header[i])
+        }
+        let ws_name = sheetname
+        // 创建工作簿对象
+        let wb = XLSX.utils.book_new()
+        let ws: any = []
+        // 创建每个工作表并设置列宽
+        for (let j = 0; j < header.length; j++) {
+          ws.push(XLSX.utils.aoa_to_sheet(data[j]))
+          let arr: any = []
+          header[j].forEach((val: any) => {
+            arr.push({
+              wpx: 120
+            })
+          })
+          ws[j]['!cols'] = arr
+        }
+        // 将工作表对象添加到工作簿中
+        for (let k = 0; k < header.length; k++) {
+          wb.SheetNames.push(ws_name[k])
+          wb.Sheets[ws_name[k]] = ws[k]
+        }
+        XLSX.writeFile(wb, filename + '.xlsx') // 导出文件
+      }
+    })
+    .finally(() => {})
+}
+
 // 数额
 const initOption = reactive({
   //标题
@@ -215,16 +292,27 @@ const initOption = reactive({
     top: 6,
     right: 8,
     feature: {
-      saveAsImage: { show: true, name: 'Revenue Spent', type: 'png', backgroundColor: '#fff' }
-      // myTool1: {
-      //   show: true,
-      //   title: 'update',
-      //   icon: 'image://' + updateIcon,
-      //   onclick: function () {
-      //     let filename = 'Revenue Spent Details ' + barName.value[0] + '-' + barName.value[barName.value.length - 1]
-      //     exportData({ filename: filename })
-      //   }
-      // }
+      saveAsImage: {
+        icon: 'path://M588.8 821.44H179.52a38.4 38.4 0 0 1-38.4-38.4v-105.088l202.432-115.584 112.96 64.576c7.872 4.48 17.6 4.48 25.472 0l400.768-228.8-0.32-0.512h0.64v-156.8a89.6 89.6 0 0 0-89.6-89.6H179.456a89.6 89.6 0 0 0-89.6 89.6v542.208a89.6 89.6 0 0 0 89.6 89.6H588.8v-51.2zM141.184 240.896a38.4 38.4 0 0 1 38.4-38.4h613.824a38.4 38.4 0 0 1 38.4 38.4v127.36L469.248 575.104 356.288 510.72a25.6 25.6 0 0 0-19.2-2.496l-6.144 2.56-189.76 108.288V240.896z m168.448 226.432c44.416 0 80.96-33.792 85.376-76.992l0.384-8.768c0-44.416-33.728-80.96-76.992-85.376l-8.768-0.448c-47.36 0-85.824 38.4-85.824 85.76l0.448 8.832c4.096 40.32 36.16 72.448 76.544 76.544l8.832 0.448z m0-51.2a34.624 34.624 0 1 1 0-69.312 34.624 34.624 0 0 1 0 69.312z m445.888 449.024a25.6 25.6 0 0 0 36.224-0.064l138.368-138.368-36.224-36.224-94.592 94.656V545.536h-51.2v239.616l-94.72-94.656-36.224 36.224 138.368 138.432z',
+        show: true,
+        name: 'Revenue Spent',
+        pixelRatio: 2
+      },
+      myTool1: {
+        show: true,
+        title: 'update',
+        icon:
+          'path://' +
+          'M571.648 956.864v-51.2H230.144a38.4 38.4 0 0 1-38.4-38.4V195.2a38.4 38.4 0 0 1 38.4-38.4h306.368v167.488c0 49.472 40.128 89.6 89.6 89.6h167.552v88.768h51.264V384h-0.448a25.472 25.472 0 0 0-7.04-13.76L580.224 113.088a25.6 25.6 0 0 0-19.84-7.424h-330.24c-49.408 0-89.6 40.128-89.6 89.6v672c0 49.472 40.128 89.6 89.6 89.6h341.504z m54.464-594.112a38.4 38.4 0 0 1-38.4-38.4V193.088l169.728 169.664H626.112z m-224.896 346.24a20.48 20.48 0 0 0 8.576-1.92 19.008 19.008 0 0 0 7.232-6.592l52.032-82.368a22.912 22.912 0 0 0 1.408-1.92c0.448-0.64 0.896-1.28 1.28-2.048l34.816 86.4a18.176 18.176 0 0 0 4.16 5.76c1.92 1.728 4.672 2.624 8.064 2.624h47.168l-56.512-128L587.52 473.6h-55.232a11.84 11.84 0 0 0-6.272 1.664 18.048 18.048 0 0 0-5.056 5.12l-41.28 67.2a59.392 59.392 0 0 0-3.84 6.272l-31.04-74.24a9.6 9.6 0 0 0-3.776-4.544c-1.408-0.96-3.84-1.472-7.232-1.472h-47.808l51.072 109.632-88.512 125.696h52.608z m357.824 240.384a25.6 25.6 0 0 0 36.224 0l147.072-147.2-36.16-36.16-103.424 103.488V610.56h-51.264v258.752l-103.36-103.36-36.288 36.224 147.2 147.136z',
+        onclick: function () {
+          let filename =
+            'Revenue Spent Details ' +
+            barName.value[0] +
+            '-' +
+            barName.value[barName.value.length - 1]
+          exportData({ filename: filename })
+        }
+      }
     },
     showTitle: false
   }
@@ -323,7 +411,7 @@ const initChart = () => {
 </script>
 
 <template>
-  <div class="com-container">
+  <div class="com-container" :class="{ 'dark-theme': themeStore.theme === 'dark' }">
     <div ref="bar_ref" id="bar_chart" :style="props.barHeight"></div>
   </div>
 </template>
@@ -334,6 +422,30 @@ const initChart = () => {
   height: 100%;
   overflow: hidden;
   position: relative;
+  // 默认(亮色)
+  :deep(.echarts-toolbox) {
+    .icon {
+      path {
+        stroke: #2b2f36 !important;
+      }
+    }
+    .icon:hover path {
+      stroke: #ff7500 !important;
+    }
+  }
+  // 暗黑模式
+  &.dark-theme {
+    :deep(.echarts-toolbox) {
+      .icon {
+        path {
+          stroke: #f0f1f3 !important; // 暗色下的默认色
+        }
+      }
+      .icon:hover path {
+        stroke: #ff7500 !important; // hover 保持橙色
+      }
+    }
+  }
 }
 #bar_chart {
   width: 100%;

+ 256 - 218
src/views/Dashboard/src/components/SellerChart.vue

@@ -1,253 +1,289 @@
-<!-- 横形柱状图 -->
 <script lang="ts" setup>
 import * as echarts from 'echarts'
 import { useThemeStore } from '@/stores/modules/theme'
-import { onMounted, ref, reactive, watch, computed } from 'vue'
+import { onMounted, ref, reactive, watch, computed, onBeforeUnmount, nextTick } from 'vue'
 import { formatNumber } from '@/utils/tools'
+
+// --- Types ---
+interface SellerItem {
+  name: string
+  value: number
+  color?: string
+  city_name?: string
+  [key: string]: any
+}
+
+interface IntervalConfig {
+  Max?: number
+  interval?: number
+}
+
+interface Props {
+  SellerData?: SellerItem[]
+  Interval?: IntervalConfig
+  saveImageName?: string
+}
+
+// --- Props & Emits ---
+const props = withDefaults(defineProps<Props>(), {
+  SellerData: () => [],
+  Interval: () => ({}),
+  saveImageName: 'chart_image'
+})
+
+const emits = defineEmits<{
+  ClickParams: [data: { name: string; cityName?: string; value: number }]
+}>()
+
 const themeStore = useThemeStore()
-const props = defineProps({
-  SellerData: Array,
-  Interval: Object,
-  saveImageName: String
+const sellerRef = ref<HTMLElement | null>(null)
+let chartInstance: echarts.ECharts | null = null
+
+// --- Computed Data ---
+// 确保数据是响应式的,但不需要在 computed 里做复杂映射,直接在 option 中处理或使用简单映射
+const sortedData = computed(() => {
+  if (!props.SellerData || props.SellerData.length === 0) return []
+  return [...props.SellerData].sort((a, b) => a.value - b.value)
 })
-const seller_data = ref(props.SellerData)
-const seller_interval = ref(props.Interval)
-const seller_ref = ref()
-watch(
-  () => props.SellerData,
-  (current) => {
-    seller_data.value = current
-    nextTick(() => {
-      initChart()
-    })
-  },
-  {
-    deep: true
-  }
-)
-watch(
-  () => props.Interval,
-  (current) => {
-    seller_interval.value = current
-    initOption.xAxis.max = Max.value
-    initOption.xAxis.interval = interval.value
-    nextTick(() => {
-      initChart()
-    })
-  },
-  {
-    deep: true
+
+const xAxisMax = computed(() => props.Interval?.Max)
+const xAxisInterval = computed(() => props.Interval?.interval)
+
+// --- Chart Initialization ---
+const initChart = () => {
+  if (!sellerRef.value) return
+
+  // 如果实例已存在,先销毁避免重复初始化导致的内存泄漏或渲染异常
+  if (chartInstance) {
+    chartInstance.dispose()
   }
-)
-// 最大值
-const Max = computed(() => {
-  return seller_interval.value?.Max
-})
-// 刻度
-const interval = computed(() => {
-  return seller_interval.value?.interval
-})
-// y轴值
-const sellerName = computed(() => {
-  return seller_data.value?.map((item: any) => {
-    return item.name
-  })
-})
-// 数额
-const sellerValue = computed(() => {
-  return seller_data.value?.map((item: any) => {
-    return item.value
-  })
-})
-// 获取数据中的color
-const ColorValue = computed(() => {
-  return seller_data.value?.map((item: any) => {
-    return item.color
-  })
-})
-const initOption = reactive({
-  // 间距
-  grid: {
-    top: '12%',
-    left: '3%',
-    right: '6%',
-    bottom: '3%',
-    containLabel: true // 距离包含坐标轴上的文字
-  },
-  // hover时的文字显示
-  tooltip: {
-    show: true,
-    backgroundColor: '#2b2f36',
-    borderColor: '#2b2f36',
-    formatter: function (params: any) {
-      var str =
-        params.name +
-        '<div style= ' +
-        'color:#FFF>' +
-        params.marker +
-        formatNumber(params.value) +
-        '</div>'
-      return str
+
+  chartInstance = echarts.init(sellerRef.value)
+  updateOption()
+
+  // 绑定点击事件
+  chartInstance.off('click') // 防止重复绑定
+  chartInstance.on('click', handleChartClick)
+
+  // 监听窗口大小变化
+  window.addEventListener('resize', handleResize)
+}
+
+const updateOption = () => {
+  if (!chartInstance) return
+
+  const isDark = themeStore.theme === 'dark'
+  const gridColor = isDark ? '#3F434A' : '#eaebed'
+  const textColor = '#B5B9BF'
+  const toolboxBorderColor = isDark ? '#f0f1f3' : '#ed6d00'
+  const toolboxBgColor = isDark ? '#2B2F36' : '#fff'
+
+  const option = {
+    grid: {
+      top: '12%',
+      left: '3%',
+      right: '6%',
+      bottom: '3%',
+      containLabel: true
     },
-    textStyle: {
-      color: '#FFF',
-      fontWeight: 700,
-      fontFamily: 'Lato-Light',
-      fontSize: '14px'
-    }
-  },
-  // X轴
-  xAxis: {
-    splitLine: {
-      lineStyle: {
-        type: 'dashed',
-        color: '#eaebed'
+    tooltip: {
+      show: true,
+      backgroundColor: '#2b2f36',
+      borderColor: '#2b2f36',
+      formatter: (params: any) => {
+        return `${params.name}<div style="color:#FFF">${params.marker}${formatNumber(params.value)}</div>`
+      },
+      textStyle: {
+        color: '#FFF',
+        fontWeight: 700,
+        fontFamily: 'Lato-Light',
+        fontSize: '14px'
       }
     },
-    type: 'value',
-    axisLine: {
-      show: false
+    xAxis: {
+      type: 'value',
+      splitLine: {
+        lineStyle: {
+          type: 'dashed',
+          color: gridColor
+        }
+      },
+      axisLine: { show: false },
+      axisLabel: {
+        fontFamily: 'Lato-Light',
+        color: textColor,
+        formatter: (value: number) => formatNumber(value, 0)
+      },
+      min: 0,
+      max: xAxisMax.value, // 直接使用 computed,echarts setOption 会处理响应式更新
+      interval: xAxisInterval.value
     },
-    axisLabel: {
-      fontFamily: 'Lato-Light',
-      color: '#B5B9BF',
-      formatter: function (value: any) {
-        return formatNumber(value, 0)
+    yAxis: {
+      type: 'category',
+      data: sortedData.value.map((item) => item.name),
+      axisLine: { show: false },
+      axisTick: { show: false },
+      axisLabel: {
+        fontFamily: 'Lato-Light',
+        color: textColor
       }
+      // 移除 yAxis 上的 tooltip 配置,通常 tooltip 在根级别或 xAxis 控制即可,避免冲突
     },
-    min: 0, // 最小值
-    max: Max.value, // 最大值
-    interval: interval.value // 刻度
-  },
-  // y轴
-  yAxis: {
-    axisLine: {
-      show: false
-    },
-    axisTick: {
-      show: false
-    },
-    axisLabel: {
-      fontFamily: 'Lato-Light',
-      color: '#B5B9BF'
-    },
-    type: 'category',
-    data: sellerName,
-    // 工具提示
-    tooltip: {
-      trigger: 'axis', // 触发类型,轴触发,axis为鼠标移到一条柱状图显示
-      axisPointer: {
-        type: 'line', // 默认为line,line为直线,cross为十字准星,shadow为阴影
-        z: 0,
-        lineStyle: {
-          color: '#FFF'
+    series: [
+      {
+        type: 'bar',
+        data: sortedData.value.map((item) => item.value),
+        barWidth: '20',
+        barCategoryGap: '0%',
+        itemStyle: {
+          color: (params: { dataIndex: number }) => {
+            const colors = sortedData.value.map((item) => item.color).filter(Boolean)
+            if (colors.length === 0) return undefined // 让 ECharts 使用默认色
+            return colors[params.dataIndex % colors.length]
+          }
+        },
+        label: {
+          show: true,
+          color: '#646A73',
+          position: 'right',
+          fontFamily: 'Lato-Light',
+          formatter: (data: { value: number }) => formatNumber(data.value)
         }
       }
-    }
-  },
-  series: [
-    {
-      type: 'bar',
-      data: sellerValue,
-      barWidth: '20',
-      itemStyle: {
-        color: function (params: { dataIndex: number }) {
-          return ColorValue.value[params.dataIndex % ColorValue.value.length]
+    ],
+    toolbox: {
+      top: 4,
+      right: 8,
+      showTitle: false,
+      iconStyle: {
+        borderColor: toolboxBorderColor
+      },
+      emphasis: {
+        iconStyle: {
+          borderColor: '#ff7500'
         }
       },
-      barCategoryGap: '0%', // 消除不同系列间的间隔
-      // 设置柱形文字的样式
-      label: {
-        show: true,
-        color: '#646A73',
-        position: 'right',
-        fontFamily: 'Lato-Light',
-        // 数据每三位加一个逗号
-        formatter: function (data: { value: { toString: () => string } }) {
-          return formatNumber(Number(data.value.toString()))
+      feature: {
+        saveAsImage: {
+          icon: 'path://M588.8 821.44H179.52a38.4 38.4 0 0 1-38.4-38.4v-105.088l202.432-115.584 112.96 64.576c7.872 4.48 17.6 4.48 25.472 0l400.768-228.8-0.32-0.512h0.64v-156.8a89.6 89.6 0 0 0-89.6-89.6H179.456a89.6 89.6 0 0 0-89.6 89.6v542.208a89.6 89.6 0 0 0 89.6 89.6H588.8v-51.2zM141.184 240.896a38.4 38.4 0 0 1 38.4-38.4h613.824a38.4 38.4 0 0 1 38.4 38.4v127.36L469.248 575.104 356.288 510.72a25.6 25.6 0 0 0-19.2-2.496l-6.144 2.56-189.76 108.288V240.896z m168.448 226.432c44.416 0 80.96-33.792 85.376-76.992l0.384-8.768c0-44.416-33.728-80.96-76.992-85.376l-8.768-0.448c-47.36 0-85.824 38.4-85.824 85.76l0.448 8.832c4.096 40.32 36.16 72.448 76.544 76.544l8.832 0.448z m0-51.2a34.624 34.624 0 1 1 0-69.312 34.624 34.624 0 0 1 0 69.312z m445.888 449.024a25.6 25.6 0 0 0 36.224-0.064l138.368-138.368-36.224-36.224-94.592 94.656V545.536h-51.2v239.616l-94.72-94.656-36.224 36.224 138.368 138.432z',
+          show: true,
+          name: props.saveImageName,
+          type: 'png',
+          backgroundColor: toolboxBgColor
         }
       }
     }
-  ],
-  toolbox: {
-    top: 4,
-    right: 8,
-    iconStyle: {
-      borderColor: '#ed6d00'
-    },
-    emphasis: {
-      iconStyle: {
-        borderColor: '#ff7500'
-      } // hover上去时的颜色
-    },
-    feature: {
-      saveAsImage: { show: true, name: props.saveImageName, type: 'png', backgroundColor: '#fff' }
-    },
-    showTitle: false
   }
-})
 
+  chartInstance.setOption(option, { notMerge: false }) // notMerge: false 允许增量更新
+}
+
+const handleResize = () => {
+  chartInstance?.resize()
+}
+
+const handleChartClick = (params: any) => {
+  const clickedItem = sortedData.value.find((item) => item.name === params.name)
+  if (clickedItem) {
+    emits('ClickParams', {
+      name: clickedItem.name,
+      cityName: clickedItem.city_name,
+      value: clickedItem.value
+    })
+  }
+}
+
+// --- Watchers ---
+// 监听数据变化
+watch(
+  () => props.SellerData,
+  () => {
+    nextTick(() => {
+      updateOption()
+    })
+  },
+  { deep: true }
+)
+
+// 监听区间配置变化
+watch(
+  () => props.Interval,
+  () => {
+    nextTick(() => {
+      updateOption()
+    })
+  },
+  { deep: true }
+)
+
+// 监听主题变化
+watch(
+  () => themeStore.theme,
+  () => {
+    nextTick(() => {
+      updateOption()
+    })
+  },
+  { immediate: true }
+)
+
+// --- Lifecycle ---
 onMounted(() => {
   initChart()
-  clickParams()
-  watch(
-    () => themeStore.theme,
-    (newVal) => {
-      if (newVal === 'dark') {
-        initOption.xAxis.splitLine.lineStyle.color = '#3F434A'
-        initOption.toolbox.iconStyle.borderColor = '#f0f1f3'
-        initOption.toolbox.feature.saveAsImage.backgroundColor = '#2B2F36'
-        initChart()
-      } else {
-        initOption.xAxis.splitLine.lineStyle.color = '#eaebed'
-        initOption.toolbox.iconStyle.borderColor = '#2B2F36'
-        initOption.toolbox.feature.saveAsImage.backgroundColor = '#fff'
-        initChart()
-      }
-    },
-    {
-      immediate: true,
-      deep: true
-    }
-  )
 })
-const emits = defineEmits(['ClickParams'])
-const paramsdata = ref()
-const paramscityname = ref()
-const clickParams = () => {
-  const seller_chart = echarts.init(seller_ref.value)
-  // 监听点击事件
-  seller_chart.on('click', function (params) {
-    paramsdata.value = params.name
-    seller_data.value?.forEach((item: any) => {
-      if (item.name == paramsdata.value) {
-        paramscityname.value = item.city_name
-      }
+
+onBeforeUnmount(() => {
+  if (chartInstance) {
+    chartInstance.dispose()
+    chartInstance = null
+  }
+  window.removeEventListener('resize', handleResize)
+})
+
+// --- Expose ---
+// 如果父组件需要访问内部数据,可以通过 emit 返回,或者暴露特定方法
+// 原代码暴露了 ref,这里为了保持兼容,创建一个响应式对象暴露
+const exposedData = ref({
+  paramsdata: '',
+  paramscityname: ''
+})
+
+// 拦截 emit 来更新暴露的数据 (可选,取决于父组件如何使用)
+const originalEmit = emits
+// 注意:在 setup 中直接重写 emits 比较麻烦,通常建议父组件直接监听事件获取数据。
+// 如果必须保持原有 expose 行为,可以在 click handler 里更新这个 ref
+const handleChartClickWithExpose = (params: any) => {
+  const clickedItem = sortedData.value.find((item) => item.name === params.name)
+  if (clickedItem) {
+    exposedData.value.paramsdata = clickedItem.name
+    exposedData.value.paramscityname = clickedItem.city_name || ''
+    emits('ClickParams', {
+      name: clickedItem.name,
+      cityName: clickedItem.city_name,
+      value: clickedItem.value
     })
-    emits('ClickParams')
-  })
-}
-const initChart = () => {
-  seller_data.value?.sort((a: any, b: any) => {
-    return a.value - b.value // 从大到小排序
-  })
-  const seller_chart = echarts.init(seller_ref.value)
-  //图表响应式
-  seller_chart.setOption(initOption)
-  //图表响应式
-  window.addEventListener('resize', () => {
-    seller_chart.resize()
-  })
+  }
 }
+// 重新绑定带 expose 逻辑的点击事件
+watch(
+  () => chartInstance,
+  (newInst) => {
+    if (newInst) {
+      newInst.off('click')
+      newInst.on('click', handleChartClickWithExpose)
+    }
+  }
+)
+
 defineExpose({
-  paramsdata,
-  paramscityname
+  paramsdata: computed(() => exposedData.value.paramsdata),
+  paramscityname: computed(() => exposedData.value.paramscityname)
 })
 </script>
 
 <template>
   <div class="com-container">
-    <div ref="seller_ref" id="seller_chart"></div>
+    <div ref="sellerRef" class="seller-chart"></div>
   </div>
 </template>
 
@@ -258,8 +294,10 @@ defineExpose({
   overflow: hidden;
   position: relative;
 }
-#seller_chart {
+
+.seller-chart {
   width: 100%;
   height: 310px;
+  // 移除 #id 选择器,使用 class 更符合 Vue 规范
 }
 </style>

TEMPAT SAMPAH
src/views/Dashboard/src/image/xiazai.png


+ 78 - 263
src/views/DestinationDelivery/src/DestinationDelivery.vue

@@ -1,97 +1,42 @@
 <script lang="ts" setup>
-import { useCalculatingHeight } from '@/hooks/calculatingHeight'
-import TableView from './components/TableView'
-import DeliveryDate from './components/DeliveryDate.vue'
+import ListView from './components/ListView.vue'
+import CalendarView from './components/CalendarView.vue'
 import { useRouter } from 'vue-router'
+import { useUserStore } from '@/stores/modules/user'
 
 const router = useRouter()
-const filterRef: Ref<HTMLElement | null> = ref(null)
-const containerHeight = useCalculatingHeight(document.documentElement, 376, [filterRef])
-const queryData = ref({
-  text_search: '',
-  created_time_start: '',
-  created_time_end: '',
-  delivery_mode: '',
-  delivery_date_start: '',
-  delivery_date_end: '',
-  filterTag: ['ALL']
-})
+const userStore = useUserStore()
 
-const modeList = [
-  {
-    label: 'Truck',
-    value: 'Truck'
-  },
-  {
-    label: 'Rail',
-    value: 'Rail'
-  }
-]
-
-const numberCards = ref([
-  {
-    label: 'Total Bookings',
-    key: 'ALL',
-    value: 0,
-    color: '#2b2f36'
-  },
-  {
-    label: 'Pending Approval',
-    value: 0,
-    color: '#edb82f',
-    icon: 'icon_time_b'
-  },
-  {
-    label: 'Approved',
-    value: 0,
-    color: '#00a870',
-    icon: 'icon_confirm_b'
-  },
-  {
-    label: 'Rejected',
-    value: 0,
-    color: '#c9353f',
-    icon: 'icon_reject_b'
-  },
-  {
-    label: 'Cancelled',
-    value: 0,
-    color: '#243041',
-    icon: 'icon_cancelled_b'
-  }
-])
-const clickCard = (filterTagItem: string) => {
-  queryData.value.filterTag.pop()
-  queryData.value.filterTag.push(filterTagItem)
-  handleSearch()
-}
-
-const DateChange = (date: any) => {
-  queryData.value.created_time_start = date ? date[0] : ''
-  queryData.value.created_time_end = date ? date[1] : ''
-}
-const deliveryDataChange = (date) => {
-  queryData.value.delivery_date_start = date ? date[0] : ''
-  queryData.value.delivery_date_end = date ? date[1] : ''
-}
+const listView = ref(null)
 
 const handleConfigurations = () => {
   router.push({ name: 'Configurations' })
 }
-const handleCreate = () => {
-  router.push({ name: 'Create New Booking' })
-}
-
-const DateStart = ref([])
-const tableRef = ref()
-
-const setNumberCards = (cards) => {
-  numberCards.value = cards
+const handleCreate = (date?: string) => {
+  router.push({
+    name: 'Create New Booking',
+    query: date ? { date } : undefined
+  })
 }
 
-const handleSearch = () => {
-  tableRef.value.SearchOperationLog()
+const jumpListPage = (date?: string) => {
+  pageType.value = 'List View'
+  nextTick(() => {
+    listView.value.searchTableData(date)
+  })
+}
+const changePageType = () => {
+  if (pageType.value === 'List View') {
+    nextTick(() => {
+      listView.value.searchTableData()
+    })
+  }
 }
+const pageType = ref('Calendar View')
+const directionOptions = [
+  { label: 'Calendar View', value: 'Calendar View', icon: 'icon_ratesheet_b' },
+  { label: 'List View', value: 'List View', icon: 'icon_date_b' }
+]
 </script>
 <template>
   <div class="destination-delivery">
@@ -102,7 +47,7 @@ const handleSearch = () => {
           style="height: 40px"
           type="default"
           @click="handleConfigurations"
-          v-if="tableRef?.isEmployeeRole === true"
+          v-if="userStore.userInfo.user_type === 'employee'"
         >
           <span style="margin-right: 4px" class="font_family icon-icon_configurations_b"></span>
           <span style="font-weight: 400">Configurations</span></el-button
@@ -110,207 +55,77 @@ const handleSearch = () => {
         <el-button
           style="height: 38px"
           class="el-button--main el-button--pain-theme"
-          @click="handleCreate"
-          v-if="tableRef?.isEmployeeRole === false"
+          @click="handleCreate()"
+          v-if="userStore.userInfo.user_type !== 'employee'"
         >
           <span style="margin-right: 4px" class="font_family icon-icon_add_b"></span>
           <span style="font-weight: 400">Create New Booking</span>
         </el-button>
       </div>
     </div>
-    <div class="display">
-      <div class="header_top">
-        <div class="date-tips_filter filter-item">
-          <span class="label">Delivery Date:</span>
-          <DeliveryDate @DateChange="deliveryDataChange" :Date="DateStart" />
-        </div>
-
-        <div class="tips_filter filter-item">
-          <span class="label">Delivery Mode:</span>
-          <el-select v-model="queryData.delivery_mode" placeholder="" clearable>
-            <el-option
-              v-for="item in modeList"
-              :key="item.value"
-              :label="item.label"
-              :value="item.value"
-            />
-          </el-select>
-        </div>
-        <div class="date-tips_filter filter-item">
-          <span class="label">Creation Date</span>
-          <CalendarDate :isShowPopupClass="true" @DateChange="DateChange"></CalendarDate>
-        </div>
-        <div class="input-tips_filter filter-item">
-          <el-input
-            placeholder="Search Question Booking No、HBOL No、MBL No、Container No、Consignee"
-            v-model="queryData.text_search"
-            class="log_input"
-          >
-            <template #prefix>
-              <span class="iconfont_icon">
-                <svg class="iconfont icon_dark" aria-hidden="true">
-                  <use xlink:href="#icon-icon_search_b"></use>
-                </svg>
-              </span>
-            </template>
-          </el-input>
-        </div>
-        <el-button class="el-button--dark" @click="handleSearch">Search</el-button>
-      </div>
-      <div class="number-cards">
-        <div
-          class="card"
-          :class="{
-            'is-active':
-              queryData.filterTag.includes(item.key as string | undefined) ||
-              queryData.filterTag.includes(item.label)
-          }"
-          @click="clickCard((item.key || item.label) as string)"
-          v-for="(item, index) in numberCards"
-          :key="index"
-        >
-          <div class="card-label">{{ item.label }}</div>
-          <div
-            class="card-value"
-            :style="{
-              color: item.label === 'Cancelled' ? `var(--color-card-number-cancelled)` : item.color
-            }"
-          >
-            {{ item.value }}
+    <div class="page-type">
+      <el-segmented v-model="pageType" @change="changePageType()" :options="directionOptions">
+        <template #default="scope">
+          <div class="flex-center">
+            <span
+              :class="['font_family', 'icon-' + scope.item.icon]"
+              style="margin-right: 4px"
+            ></span>
+            <div>{{ scope.item.label }}</div>
           </div>
-          <div class="icon-box" v-if="item.icon">
-            <span class="font_family" :class="'icon-' + item.icon"></span>
-          </div>
-        </div>
-      </div>
+        </template>
+      </el-segmented>
     </div>
-    <TableView
-      @get-number-cards="setNumberCards"
-      :height="containerHeight"
-      :queryData="queryData"
-      ref="tableRef"
-    ></TableView>
+
+    <ListView ref="listView" v-if="pageType === 'List View'"></ListView>
+    <CalendarView
+      @add="handleCreate"
+      @jumpListPage="jumpListPage"
+      :isEmployeeRole="listView?.isEmployeeRole"
+      v-if="pageType === 'Calendar View'"
+    ></CalendarView>
   </div>
 </template>
 
 <style lang="scss" scoped>
+.destination-delivery {
+  position: relative;
+  padding-bottom: 40px;
+  background-color: var(--color-mode);
+}
 .header {
+  position: sticky;
+  top: 0;
+  z-index: 100;
   display: flex;
+  align-items: center;
+  justify-content: space-between;
   height: 68px;
+  padding: 0 24px;
   border-bottom: 1px solid var(--color-border);
   font-size: var(--font-size-6);
   font-weight: 700;
-  padding: 0 24px;
-  align-items: center;
-  justify-content: space-between;
-}
-.header_top {
-  margin-bottom: 8px;
-  padding-right: 8px;
-  display: flex;
-  align-items: flex-end;
+  background-color: var(--color-mode);
 }
-.number-cards {
-  display: flex;
-  justify-content: space-between;
-  margin: 8px 0;
-  gap: 8px;
-  .card {
-    position: relative;
-    flex: 1;
-    height: 80px;
-    padding: 12px 16px;
-    background-color: var(--color-email-bg);
-    border-radius: 12px;
-    & > .icon-box {
-      position: absolute;
-      right: 16px;
-      top: 12px;
-      width: 24px;
-      height: 24px;
-      border-radius: 6px;
-      background-color: var(--color-card-icon-box-bg);
-      text-align: center;
-      line-height: 24px;
-    }
-    &.is-active {
-      background-color: var(--color-mune-active-bg);
-      .card-label,
-      .card-value {
-        color: var(--color-theme) !important;
-      }
-      .icon-box {
-        background-color: rgba(#ff7500, 0.1);
-        span {
-          color: var(--color-theme);
-        }
-      }
-    }
-
-    .card-value {
-      margin-top: 4px;
-      font-size: 32px;
-      font-weight: 700;
-    }
+.page-type {
+  position: relative;
+  z-index: 10;
+  width: 300px;
+  margin: 10px 24px;
+  .el-segmented {
+    height: 40px;
+    --el-segmented-item-selected-color: var(--color-neutral-1);
+    --el-segmented-item-selected-bg-color: var(--color-el-segmented-checked-bg);
+    --el-segmented-bg-color: var(--color-el-segmented-bg);
+    --el-border-radius-base: 6px;
   }
-}
-
-.display {
-  border: 1px solid var(--color-border);
-  border-width: 0 0 1px 0;
-  padding: 0 24px;
-}
-:deep(.el-select__placeholder.is-transparent span) {
-  color: var(--tag-info-text-color) !important;
-}
-:deep(.ETD_title) {
-  margin-bottom: 0;
-}
-:deep(.ant-picker-range) {
-  width: 250px !important;
-  height: 32px;
-  background-color: var(--color-mode) !important;
-}
-.filter-item {
-  flex: 1;
-  display: flex;
-  flex-direction: column;
-  align-items: flex-end;
-  justify-content: flex-end;
-  margin-right: 8px;
-  .label {
-    align-self: flex-start;
-    font-size: 12px;
-    margin-bottom: 3px;
+  .flex-center {
+    display: flex;
+    align-items: center;
+    justify-content: center;
   }
-}
-.tips_filter {
-  min-width: 170px;
-  max-width: 220px;
-}
-.input-tips_filter {
-  max-width: 540px;
-  :deep(.el-input__wrapper) {
-    height: 32px;
-    input {
-      width: 100%;
-      white-space: nowrap; /* 防止换行 */
-      overflow: hidden; /* 超出部分隐藏 */
-      text-overflow: ellipsis; /* 超出部分显示为省略号 */
-    }
+  :deep(.el-segmented__item-label) {
+    color: var(--color-neutral-2);
   }
 }
-.date-tips_filter {
-  max-width: 250px;
-  height: 56px;
-}
-
-.destination-delivery {
-  position: relative;
-  background-color: var(--color-mode);
-}
-:deep(.log_input .el-input__wrapper) {
-  box-shadow: 0 0 0 1px var(--color-select-border);
-  border-radius: 6px;
-}
 </style>

+ 263 - 0
src/views/DestinationDelivery/src/components/CalendarTagDetailDialog.vue

@@ -0,0 +1,263 @@
+<script setup lang="ts">
+import { autoWidth } from '@/utils/table'
+import { type VxeGridInstance, type VxeGridProps } from 'vxe-table'
+import { useRowClickStyle } from '@/hooks/rowClickStyle'
+import { formatTimezone } from '@/utils/tools'
+import dayjs from 'dayjs'
+import { useThemeStore } from '@/stores/modules/theme'
+
+const dialogVisible = ref(false)
+
+const props = defineProps({
+  data: Object
+})
+const tableData = ref<VxeGridProps<any>>({
+  minHeight: 150,
+  maxHeight: 420,
+  border: true,
+  round: true,
+  columns: [],
+  data: [],
+  cellConfig: {
+    height: 40
+  },
+  headerCellConfig: {
+    height: 40
+  },
+  scrollY: { enabled: true, oSize: 20, gt: 30 },
+  emptyText: ' ',
+  showHeaderOverflow: true,
+  showOverflow: true,
+  headerRowStyle: {
+    backgroundColor: 'var(--color-table-header-bg)'
+  },
+  columnConfig: { resizable: true, useKey: true },
+  rowConfig: { isHover: true }
+})
+const tableRef = ref<VxeGridInstance | null>(null)
+// 实现行点击样式
+useRowClickStyle(tableRef)
+
+const getTableColumns = () => {
+  $api
+    .BookingTableColumn({
+      reset: 'yes'
+    })
+    .then((res: any) => {
+      if (res.code === 200) {
+        tableData.value.columns = [...handleColumns(res.data.TrackingTableColumns)]
+      }
+    })
+}
+onMounted(() => {
+  getTableColumns()
+})
+const handleColumns = (columns: any) => {
+  const newColumns = columns.map((item: any) => {
+    let curColumn: any = {
+      title: item.title,
+      field: item.field,
+      width: '150px'
+    }
+    // 格式化
+    if (item.formatter === 'date') {
+      curColumn = {
+        ...curColumn,
+        formatter: ({ cellValue }: any) => formatTimezone(cellValue)
+      }
+    } else if (item.type === 'link') {
+      curColumn = {
+        ...curColumn,
+        slots: { default: 'trackingNo' }
+      }
+    } else if (item.type == 'recommend') {
+      curColumn = {
+        ...curColumn,
+        formatter: ({ cellValue }: any) => {
+          if (!cellValue) return ''
+          const array = cellValue?.split('-')
+          return `${formatTimezone(array[0])} - ${formatTimezone(array[1])}`
+        }
+      }
+    } else if (item.type === 'download') {
+      curColumn = {
+        ...curColumn,
+        slots: { default: 'download' }
+      }
+    }
+    return curColumn
+  })
+  return newColumns
+}
+
+const title = ref('')
+const pageData = ref()
+const tagType = ref()
+const openDialog = (type, date, data) => {
+  dialogVisible.value = true
+  pageData.value = data
+  tagType.value = type
+
+  title.value = `Recommended Delivery Shipments for ${dayjs(date).format('YYYY-MM-DD')}`
+  tableData.value.data = data['shipmentDetail']
+  nextTick(() => {
+    tableRef.value &&
+      autoWidth(tableData.value, tableRef.value, {
+        packing_list: 190,
+        commercial_invoice: 180
+      })
+  })
+}
+const themeStore = useThemeStore()
+const rowStyle = ({ row }) => {
+  if (row.is_ending) {
+    return {
+      backgroundColor:
+        themeStore.theme === 'dark' ? 'rgba(255, 18, 28, 0.2)' : 'rgba(193, 0, 8, 0.1)'
+    }
+  }
+}
+function base64ToBlob(base64, mimeType) {
+  const byteString = atob(base64.split(',')[0].startsWith('data:') ? base64.split(',')[1] : base64)
+  const ab = new ArrayBuffer(byteString.length)
+  const ia = new Uint8Array(ab)
+  for (let i = 0; i < byteString.length; i++) {
+    ia[i] = byteString.charCodeAt(i)
+  }
+  return new Blob([ia], { type: mimeType })
+}
+const handleDownload = (serialNo: string, field: string) => {
+  const fileType = field === 'commercial_invoice' ? 'C/I' : 'Packing List'
+  $api
+    .downloadBookingTableFile({
+      serial_no: serialNo,
+      file_type: fileType
+    })
+    .then((res: any) => {
+      if (res.code === 200) {
+        // 使用
+        const base64 = res.data.data // 纯 base64 字符串,不含 data: 前缀
+        const mimeType = 'application/octet-stream'
+        const fileName = res.data.filename || 'download'
+
+        const blob = base64ToBlob(base64, mimeType)
+        const url = URL.createObjectURL(blob)
+
+        const link = document.createElement('a')
+        link.href = url
+        link.download = fileName
+        document.body.appendChild(link)
+        link.click()
+        document.body.removeChild(link)
+
+        // 可选:下载完成后释放内存
+        link.onclick = () => {
+          setTimeout(() => {
+            URL.revokeObjectURL(url) // 释放对象 URL
+          }, 100)
+        }
+      }
+    })
+}
+
+const clearData = () => {
+  tableData.value.data = []
+}
+defineExpose({
+  openDialog
+})
+</script>
+
+<template>
+  <div>
+    <el-dialog v-model="dialogVisible" width="80%" @closed="clearData">
+      <template #header>
+        <div class="header-content">
+          <span class="title">{{ title }}</span>
+          <div class="ending-tag tag-style">
+            <span
+              class="font_family icon-icon_delay_b1"
+              style="margin-right: 3px; font-size: 13px"
+            ></span>
+            <span class="type">{{ pageData?.endingNumber }} Free Storage Period Ends</span>
+          </div>
+        </div>
+      </template>
+      <span style="display: inline-block; color: var(--color-neutral-2)"
+        >Total:
+        {{ pageData?.shipmentNumber || 0 }}
+        shipments</span
+      >
+      <span style="color: var(--color-neutral-2)"> | </span>
+      <span style="color: var(--color-neutral-2)"
+        >Total Cartons: {{ pageData?.shipmentCtns || 0 }} ctns</span
+      >
+      <vxe-grid style="margin-top: 8px" ref="tableRef" v-bind="tableData" :row-style="rowStyle">
+        <template #download="{ row, column }">
+          <div class="download-btn" @click="handleDownload(row.h_serial_no, column.field)">
+            <span class="font_family icon-icon_download_b icon-style"> </span>
+            <span
+              >{{ row.h_bol
+              }}{{ column.field === 'commercial_invoice' ? '.CI.zip' : '._PL.zip' }}</span
+            >
+          </div>
+        </template>
+        <template #empty>
+          <div class="empty">No data</div>
+        </template>
+      </vxe-grid>
+    </el-dialog>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.header-content {
+  display: flex;
+  align-items: center;
+  gap: 8px;
+  .title {
+    font-size: 18px;
+    font-weight: 700;
+  }
+}
+
+.ending-tag {
+  height: 24px;
+  width: auto;
+  border: 0.5px dashed var(--color-calendar-ending-tag-border);
+  background-color: var(--color-calendar-ending-tag-bg);
+  .font_family,
+  .type {
+    color: var(--color-calendar-ending-tag-text) !important;
+  }
+}
+.tag-style {
+  display: flex;
+  align-items: center;
+  padding-left: 6px;
+  padding-right: 4px;
+  height: 24px;
+  border-radius: 3px;
+  overflow: hidden;
+
+  .font_family {
+    margin-right: 4px;
+  }
+  .type {
+    font-size: 10px;
+    font-weight: 700;
+    line-height: 16px;
+  }
+}
+.download-btn {
+  cursor: pointer;
+
+  &:hover,
+  &:focus {
+    span,
+    .icon-style {
+      color: var(--color-theme) !important;
+    }
+  }
+}
+</style>

+ 530 - 0
src/views/DestinationDelivery/src/components/CalendarView.vue

@@ -0,0 +1,530 @@
+<script setup lang="ts">
+import { ref, computed, onMounted } from 'vue'
+import type { Dayjs } from 'dayjs'
+import dayjs from 'dayjs'
+import CalendarTagDetailDialog from './CalendarTagDetailDialog.vue'
+import { useUserStore } from '@/stores/modules/user'
+
+const userStore = useUserStore()
+
+// 控制日历显示的月份(受控状态)
+const displayMonth = ref<Dayjs>(dayjs())
+
+// 年份选项
+const yearOptions = computed(() => {
+  const currentYear = new Date().getFullYear()
+  const startYear = 1950 // 你可以改成任意起始年,比如 1990
+  const years = []
+  for (let y = currentYear; y >= startYear; y--) {
+    years.push(y)
+  }
+  return years //
+})
+
+const calendarData = ref({})
+
+const getDataByDate = (date: Dayjs, key: string) => {
+  return calendarData.value[date.format('YYYY-MM-DD')]?.[key] ?? 0
+}
+
+const emit = defineEmits(['add', 'jumpListPage'])
+const calendarLoading = ref(false)
+const handleAddClick = (date) => {
+  emit('add', dayjs(date).format('YYYY-MM-DD'))
+}
+
+const getPageData = () => {
+  calendarLoading.value = true
+  $api
+    .getDeliveryCalendarData({ month: displayMonth.value.format('MM/YYYY') })
+    .then((res) => {
+      if (res.code === 200) {
+        calendarData.value = res.data
+      }
+    })
+    .finally(() => {
+      calendarLoading.value = false
+    })
+}
+
+onMounted(() => {
+  getPageData()
+})
+
+// 👇 关键:监听 panelChange(月份/年份切换)
+const onPanelChange = (value: Dayjs) => {
+  // 同步更新 displayMonth,确保受控
+  displayMonth.value = value
+  getPageData()
+}
+
+const handleYearChange = (year: number, onChange: (date: Dayjs) => void) => {
+  const newDate = displayMonth.value.clone().year(year)
+  onChange(newDate)
+  displayMonth.value = newDate
+}
+
+// 月份切换
+const handleMonthChange = (month: number, onChange: (date: Dayjs) => void) => {
+  const newDate = displayMonth.value.clone().month(month - 1)
+  onChange(newDate)
+  displayMonth.value = newDate
+}
+
+const calendarTagDialog = ref()
+
+const handleTagClick = (type, date) => {
+  calendarTagDialog.value.openDialog(type, date, calendarData.value[date.format('YYYY-MM-DD')])
+}
+
+const jumpListPage = (date) => {
+  emit('jumpListPage', date.format('YYYY-MM-DD'))
+}
+</script>
+
+<template>
+  <div class="calendar-container">
+    <!-- 👇 绑定 :value + 监听 @panelChange -->
+    <a-calendar
+      :value="displayMonth"
+      v-vloading="calendarLoading"
+      fullscreen
+      @panelChange="onPanelChange"
+    >
+      <!-- 自定义头部:value 来自 displayMonth(已同步) -->
+      <template #headerRender="{ value, onChange }">
+        <div class="custom-header">
+          <div class="label-type destination-booking">
+            <div class="sign"></div>
+            <div class="label">Destination Booking</div>
+          </div>
+          <div class="label-type recommended-delivery-date">
+            <div class="sign"></div>
+            <div class="label">Recommended Delivery Date</div>
+          </div>
+          <div class="label-type free-storage-period-ends">
+            <div class="sign"></div>
+            <div class="label">Free Storage Period Ends</div>
+          </div>
+          <div class="grid-lines"></div>
+          <a-select
+            :value="value.year()"
+            style="width: 180px; margin-right: 8px"
+            @change="(year) => handleYearChange(year, onChange)"
+          >
+            <a-select-option v-for="y in yearOptions" :key="y" :value="y">
+              {{ y }}
+            </a-select-option>
+          </a-select>
+
+          <a-select
+            :value="value.month() + 1"
+            style="width: 80px"
+            @change="(month) => handleMonthChange(month, onChange)"
+          >
+            <a-select-option v-for="m in 12" :key="m" :value="m">
+              {{
+                [
+                  'Jan',
+                  'Feb',
+                  'Mar',
+                  'Apr',
+                  'May',
+                  'Jun',
+                  'Jul',
+                  'Aug',
+                  'Sep',
+                  'Oct',
+                  'Nov',
+                  'Dec'
+                ][m - 1]
+              }}
+            </a-select-option>
+          </a-select>
+        </div>
+      </template>
+
+      <template #dateCellRender="{ current }">
+        <ul class="events">
+          <!-- 如果不为当前月份的日期,则不显示标签 -->
+          <div
+            class="tags-details"
+            v-if="
+              calendarData?.[dayjs(current).format('YYYY-MM-DD')] &&
+              dayjs(current).isSame(dayjs(displayMonth), 'month')
+            "
+          >
+            <div
+              v-if="getDataByDate(current, 'endingNumber') || getDataByDate(current, 'endingCtns')"
+              class="ending-tag tag-style"
+              @click="handleTagClick('ending', current)"
+            >
+              <span
+                class="font_family icon-icon_delay_b1"
+                style="margin-right: 3px; font-size: 13px"
+              ></span>
+              <span class="type">{{ getDataByDate(current, 'endingNumber') }} Ending</span>
+              <span class="ctns-tag">{{ getDataByDate(current, 'endingCtns') }} ctns</span>
+            </div>
+            <div
+              v-if="
+                getDataByDate(current, 'shipmentNumber') || getDataByDate(current, 'shipmentCtns')
+              "
+              class="delivery-tag"
+              @click="handleTagClick('delivery', current)"
+            >
+              <div class="label">Recommended Delivery</div>
+              <div class="tag-style">
+                <span class="font_family icon-icon_road__booking_b" style="font-size: 12px"></span>
+                <span class="type">{{ getDataByDate(current, 'shipmentNumber') }} Shipments</span>
+                <span class="ctns-tag">{{ getDataByDate(current, 'shipmentCtns') }} ctns</span>
+              </div>
+            </div>
+            <div
+              class="booking-tag"
+              v-if="
+                getDataByDate(current, 'bookingNumber') || getDataByDate(current, 'bookingCtns')
+              "
+              @click="jumpListPage(current)"
+            >
+              <div class="label">Destination Booking</div>
+              <div class="tag-style">
+                <span class="font_family icon-icon_booking_order_b" style="font-size: 12px"></span>
+                <span class="type">{{ getDataByDate(current, 'bookingNumber') }} Bookings</span>
+                <span class="ctns-tag">{{ getDataByDate(current, 'bookingCtns') }} ctns</span>
+              </div>
+            </div>
+          </div>
+          <!-- 新增图标 -->
+          <div
+            class="add-icon"
+            @click.stop="handleAddClick(current)"
+            v-if="userStore.userInfo.user_type.toLowerCase() !== 'employee'"
+          >
+            <span class="font_family icon-icon_add_b"></span>
+          </div>
+        </ul>
+      </template>
+    </a-calendar>
+    <CalendarTagDetailDialog ref="calendarTagDialog"></CalendarTagDetailDialog>
+  </div>
+</template>
+
+<style scoped lang="scss">
+.calendar-container {
+  // position: relative;
+  // z-index: 99;
+  margin-top: -46px;
+  padding: 0 24px;
+}
+
+.custom-header {
+  display: flex;
+  justify-content: flex-end;
+  align-items: center;
+  padding: 2px 0;
+  .label-type {
+    display: flex;
+    align-items: center;
+    margin-right: 24px;
+
+    .sign {
+      width: 10px;
+      height: 10px;
+      margin-right: 4px;
+      border-radius: 3px;
+    }
+  }
+  .destination-booking {
+    .sign {
+      background-color: var(--color-delivery-calendar-booking-sign-bg);
+      border: 1px dashed var(--color-delivery-calendar-booking-sign-border);
+    }
+    .label {
+      color: var(--color-delivery-calendar-booking-label-text);
+    }
+  }
+  .recommended-delivery-date {
+    .sign {
+      background-color: var(--color-delivery-calendar-delivery-date-sign-bg);
+      border: 1px dashed var(--color-delivery-calendar-delivery-date-sign-border);
+    }
+    .label {
+      color: var(--color-delivery-calendar-delivery-date-label-text);
+    }
+  }
+  .free-storage-period-ends {
+    margin-right: 8px;
+    .sign {
+      background-color: var(--color-calendar-ending-tag-bg);
+      border: 1px dashed var(--color-calendar-ending-tag-border);
+    }
+    .label {
+      color: var(--color-delivery-calendar-ends-label-text);
+    }
+  }
+  .grid-lines {
+    height: 18px;
+    width: 2px;
+    margin-right: 8px;
+    background-color: var(--color-border);
+  }
+}
+
+/* ===== 深色主题覆盖(保持原样)===== */
+:deep(.ant-picker-calendar) {
+  background-color: var(--color-mode);
+  color: #e0e0e0;
+  border: none;
+}
+
+:deep(.ant-picker-calendar-header) {
+  background-color: var(--color-mode);
+}
+
+:deep(.ant-picker-calendar-header .ant-picker-year-select),
+:deep(.ant-picker-calendar-header .ant-picker-month-select) {
+  border: 1px solid #444;
+  color: #e0e0e0;
+}
+
+:deep(.ant-picker-calendar-body) {
+  // background-color: #1a1a1a;
+}
+
+:deep(.ant-picker-cell) {
+  background-color: var(--color-calendar-cell-bg) !important;
+  border: 1px solid var(--color-border) !important;
+}
+
+:deep(.ant-picker-calendar-date) {
+  height: 143px !important;
+  display: block !important;
+  border-top: 0 !important;
+  margin: 0 !important;
+  overflow: hidden;
+  transition: none !important;
+}
+
+:deep(.ant-picker-content) {
+  border-radius: 6px;
+  overflow: hidden;
+}
+:deep(.ant-picker-content thead) {
+  border-radius: 6px;
+  overflow: hidden;
+  border: 1px solid var(--color-border) !important;
+}
+:deep(.ant-picker-content th) {
+  height: 24px !important;
+  padding: 0 !important;
+  text-align: center;
+  color: var(--color-calendar-disabled-date-text) !important;
+  background-color: var(--color-delivery-calendar-th-bg) !important;
+}
+
+:deep(.ant-picker-calendar-date-value) {
+  display: block;
+  font-size: 14px;
+  line-height: 1.2;
+  color: #e0e0e0;
+  margin-bottom: 4px;
+  text-align: left;
+}
+
+:deep(.ant-picker-cell .ant-picker-calendar-date-value) {
+  color: var(--color-n--color-neutral-1) !important;
+}
+
+/* 彻底清除选中状态 */
+:deep(.ant-picker-calendar-date-selected),
+:deep(.ant-picker-cell-selected .ant-picker-calendar-date) {
+  background: transparent !important;
+  color: var(--color-neutral-1) !important;
+  box-shadow: none !important;
+}
+:deep(td.ant-picker-cell-selected) {
+  &:hover {
+    background: var(--color-calendar-selected-cell-bg) !important;
+  }
+}
+// 不是当月日期时的样式
+:deep(
+  td.ant-picker-cell:not(.ant-picker-cell-in-view):not(.custom-delivery-calendar .ant-picker-cell)
+) {
+  background-color: rgba(0, 0, 0, 0) !important;
+  .ant-picker-calendar-date-value {
+    color: var(--color-calendar-disabled-date-text) !important;
+  }
+}
+
+:deep(
+  td.ant-picker-cell:hover:not(.ant-picker-cell-in-view):not(
+      .custom-delivery-calendar .ant-picker-cell
+    )
+) {
+  background-color: rgba(249, 250, 252, 0.1) !important;
+}
+:deep(.ant-picker-cell:hover) {
+  background-color: rgba(249, 250, 252, 0.1) !important;
+  .ant-picker-calendar-date-value {
+    color: var(--color-neutral-1) !important;
+  }
+}
+
+/* 同时清除 hover 时的干扰 */
+:deep(.ant-picker-cell-selected:hover .ant-picker-calendar-date) {
+  background: transparent !important;
+}
+
+.events {
+  display: flex;
+  flex-direction: column;
+  justify-content: flex-end;
+  list-style: none;
+  margin: 0;
+  padding: 0;
+  padding-bottom: 6px;
+  height: 110px;
+  overflow-y: auto;
+  font-size: 12px;
+}
+
+.add-icon {
+  position: absolute;
+  top: 8px;
+  right: 8px;
+  background-color: var(--color-theme);
+  border-radius: 50%;
+  width: 16px;
+  height: 16px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  opacity: 0;
+  transition: opacity 0.3s ease;
+
+  .font_family {
+    display: inline-block;
+    font-size: 12px;
+    color: #fff !important;
+  }
+}
+
+.ant-picker-cell-in-view:hover .add-icon {
+  opacity: 1;
+}
+
+.events .tags-details {
+  display: flex;
+  align-items: center;
+  gap: 4px;
+  margin-top: 2px;
+  color: #ccc;
+  text-align: left;
+}
+
+.tags-details {
+  display: flex;
+  flex-direction: column;
+}
+.tag-style {
+  display: flex;
+  align-items: center;
+  padding-left: 6px;
+  height: 15px;
+  border-radius: 3px;
+  overflow: hidden;
+
+  .font_family {
+    margin-right: 4px;
+  }
+  .type {
+    font-size: 10px;
+    font-weight: 700;
+    line-height: 16px;
+  }
+  .ctns-tag {
+    height: 12px;
+    margin-left: 6px;
+    padding: 0 3px;
+    padding-top: 0.5px;
+    font-size: 8px;
+    border-radius: 3px;
+    background-color: #f3cfd0;
+  }
+}
+.delivery-tag,
+.booking-tag {
+  height: 36px;
+  width: 100%;
+  padding-top: 4px;
+  border-radius: 3px;
+  .label {
+    height: 12px;
+    padding-left: 6px;
+    font-size: 8px;
+    line-height: 12px;
+  }
+  .ctns-tag {
+    padding-top: 1px;
+  }
+}
+.delivery-tag {
+  border: 0.5px dashed var(--color-calendar-delivery-tag-border);
+  background-color: var(--color-calendar-delivery-tag-bg);
+  .label {
+    color: var(--color-calendar-delivery-tag-label-text);
+  }
+  .font_family,
+  .type,
+  .ctns-tag {
+    color: var(--color-calendar-delivery-tag-text) !important;
+  }
+  .ctns-tag {
+    background-color: var(--color-calendar-delivery-tag-bg);
+  }
+}
+.booking-tag {
+  background-color: var(--color-calendar-booking-tag-bg);
+  .label {
+    color: var(--color-calendar-booking-tag-label-text);
+  }
+  .font_family,
+  .type,
+  .ctns-tag {
+    color: var(--color-calendar-booking-tag-text) !important;
+  }
+  .ctns-tag {
+    background-color: var(--color-calendar-booking-tag-bg);
+  }
+}
+.ending-tag {
+  height: 24px;
+  width: 100%;
+  border: 0.5px dashed var(--color-calendar-ending-tag-border);
+  background-color: var(--color-calendar-ending-tag-bg);
+  .font_family,
+  .type,
+  .ctns-tag {
+    color: var(--color-calendar-ending-tag-text) !important;
+  }
+  .ctns-tag {
+    background-color: var(--color-calendar-ending-tag-bg);
+  }
+}
+:deep(.ant-picker-calendar-date-content) {
+  height: 110px !important;
+}
+</style>
+
+<style lang="scss">
+.ant-picker-calendar.ant-picker-calendar-full .ant-picker-panel {
+  background-color: var(--color-mode) !important;
+  span,
+  td,
+  th {
+    color: var(--color-neutral-1);
+  }
+}
+</style>

+ 24 - 12
src/views/DestinationDelivery/src/components/ConfiguRations/src/components/CreateNewRule.vue

@@ -346,12 +346,8 @@ onMounted(() => {
             </div>
           </template>
           <template #header>
-            <div class="cancel_header">
-              <span class="iconfont_icon iconfont_warning">
-                <svg class="iconfont icon_warning" aria-hidden="true">
-                  <use xlink:href="#icon-icon_tipsfilled_b"></use>
-                </svg>
-              </span>
+            <div class="dialog-header warning-header">
+              <span class="font_family icon-icon_tipsfilled_b"></span>
               Unsaved Changes
             </div>
           </template>
@@ -372,12 +368,8 @@ onMounted(() => {
             </div>
           </template>
           <template #header>
-            <div class="cancel_header">
-              <span class="iconfont_icon iconfont_warning">
-                <svg class="iconfont icon_danger" aria-hidden="true">
-                  <use xlink:href="#icon-icon_fail_fill_b"></use>
-                </svg>
-              </span>
+            <div class="unable-save-header dialog-header">
+              <span class="font_family icon-icon_fail_fill_b"></span>
               Unable to Save
             </div>
           </template>
@@ -611,4 +603,24 @@ onMounted(() => {
 :deep(.el-dialog__body) {
   font-weight: 400;
 }
+
+.dialog-header {
+  display: flex;
+  align-items: center;
+  .font_family {
+    font-size: 14px;
+    width: 16px;
+    height: 16px;
+    border-radius: 24px;
+    margin-right: 4px;
+  }
+}
+
+.unable-save-header .font_family {
+  color: #b53039;
+}
+
+div.warning-header .font_family {
+  color: #e9b227;
+}
 </style>

+ 192 - 107
src/views/DestinationDelivery/src/components/CreateNewBooking/src/CreateNewbooking.vue

@@ -1,29 +1,30 @@
 <script setup lang="ts">
 import CalendarDate from '@/components/DateRange/src/components/CalendarDate.vue'
-import AutoSelect from '@/components/AutoSelect/src/AutoSelect.vue'
 import NewbookingTable from './components/NewbookingTable.vue'
 import AddNewAddress from './images/default_add_address@2x.png'
 import NotAvailable from './images/default_destination_not_available@2x.png'
 import NotShipment from './images/default_no_shipment@2x.png'
 import submitsucessful from './images/icon_success_big@2x.png'
 import { useUserStore } from '@/stores/modules/user'
-import { useRouter } from 'vue-router'
+import { useRouter, useRoute } from 'vue-router'
 import { ElMessage } from 'element-plus'
+import dayjs from 'dayjs'
 
 const userStore = useUserStore()
 const router = useRouter()
-const { currentRoute } = router
-const { value } = currentRoute
-const { query } = value
-const { a } = query
+const route = useRoute()
+const a = route.query.a
 
 const CreateNewBOokingSearch = ref('')
 const status = ref('')
 const booking = ref('')
-const DateValue = ref('')
+const DateValue = ref(
+  route.query.date && dayjs(route.query.date as string).isValid()
+    ? dayjs(route.query.date as string).format('YYYY.MM.DD')
+    : ''
+)
 const DeliveryTime = ref('')
 const bookingTableRef = ref()
-const VesselName = ref([])
 const VesselNametest = ref('')
 const ShipperValue = ref('')
 const ConsigneeValue = ref('')
@@ -53,18 +54,15 @@ const recommendateWarning = ref('')
 const ATATimeList = ref(null)
 const ETATimeList = ref(null)
 const Addressradio = ref()
-const LocationName = ref('')
 const AddressLine1 = ref('')
 const AddressLine2 = ref('')
 const AddressLine3 = ref('')
 const AddressLine4 = ref('')
 const CountryCode = ref('')
 const CityCode = ref('')
-const CountryCity = ref('')
 const PostalCode = ref('')
 const ContactPerson = ref('')
 const ContactNumber = ref('')
-const instructions = ref('')
 const modetypeValue = ref('Truck')
 const Requirements = ref('')
 const Modification = ref()
@@ -215,10 +213,8 @@ interface CountryItem {
   value: string
   label: string
 }
-const countrys = ref<CountryItem[]>([])
 const Countryoptions = ref<CountryItem[]>([])
 const Countryloading = ref(false)
-const city = ref<CountryItem[]>([])
 const Cityoptions = ref<CountryItem[]>([])
 const cityloading = ref(false)
 const querySearchCountry = (query: string) => {
@@ -260,9 +256,13 @@ const getCurrentStyle = (current: any) => {
   const dateString = current.format('YYYY.MM.DD')
   if (specialDates.value != undefined && specialDates.value.includes(dateString)) {
     return {
-      background: '#b3e5d4',
+      background: 'var(--color-delivery-date-picker-cell-bg)',
       borderRadius: '6px',
-      color: `var(--color-neutral-1)`
+      color: `var(--color-neutral-1)`,
+      margin: 'auto',
+      width: '32px',
+      height: '32px',
+      lineHeight: '33px'
     }
   }
   // 默认样式
@@ -368,7 +368,7 @@ const SaveNewAddress = () => {
 }
 // 点击按钮
 const handleclickbutton = (val: any) => {
-  Requirements.value = Requirements.value + val
+  Requirements.value = Requirements.value ? Requirements.value + ', ' + val : val
 }
 let delivery_address = ''
 const changeAddressRadio = () => {
@@ -386,6 +386,7 @@ let checkShipmentsdata = []
 let checkShipmentsInfo = {}
 let checkShipmentsSubmitInfoData = {}
 const getInitBookingData = () => {
+  bookingTableRef.value.tableLoadingTable = true
   $api
     .InitCreateBooking({
       serial_no: a != undefined ? a : ''
@@ -454,6 +455,12 @@ const getInitBookingData = () => {
         }
       }
     })
+    .finally(() => {
+      bookingTableRef.value.tableLoadingTable = false
+    })
+}
+const clearManageDialog = () => {
+  ManageAddressList.value = []
 }
 // 查询Shipments
 const SearchShipment = () => {
@@ -533,6 +540,7 @@ const selectChangeEvent = (val: any, date: any, submitInfo: any) => {
   }
 }
 
+const manageLoading = ref(false)
 // 点击 Address Book
 const handleClickAddress = () => {
   if (!isAddNewAddressVisible.value) {
@@ -542,6 +550,7 @@ const handleClickAddress = () => {
     (item) => item.contact_type !== 'Delete'
   )
   if (a == undefined) {
+    manageLoading.value = true
     $api
       .getAddressBookList({
         ...getAddressListData.value
@@ -555,11 +564,20 @@ const handleClickAddress = () => {
           ManageVisible.value = true
         }
       })
+      .finally(() => {
+        manageLoading.value = false
+      })
   }
 }
 
 //选择日期
 const changeDatePicker = (val: any) => {
+  if (val === null) {
+    isRecommendDate.value = false
+    recommendateWarning.value = ''
+    isConsistent.value = false
+    return
+  }
   if (specialDates.value.length != 0) {
     if (isConsistent.value) {
       isRecommendDate.value = true
@@ -688,6 +706,12 @@ const SubmitBooking = () => {
     })
 }
 
+const ctnsCount = computed(() => {
+  return bookingTableRef.value?.getTableCheckedRows().reduce((total, row) => {
+    return total + (Number(row.pakages) || 0)
+  }, 0)
+})
+
 onMounted(() => {
   getInitBookingData()
 })
@@ -699,6 +723,15 @@ onMounted(() => {
       <div v-if="a == undefined">Create New Booking</div>
       <div v-else>Modify Booking</div>
       <div class="flex">
+        <div class="select-info">
+          <span style="color: var(--color-neutral-2)">Selected: </span>
+          <span
+            >{{ bookingTableRef?.getTableCheckedRows().length }} Shipments|{{
+              ctnsCount
+            }}
+            ctns</span
+          >
+        </div>
         <el-button @click="CancelRulesVisible = true" class="el-button--default create-button"
           ><span class="font_family icon-icon_return_b"></span> Cancel</el-button
         >
@@ -718,31 +751,29 @@ onMounted(() => {
     </div>
     <el-divider v-if="a != undefined" style="margin: 8px 0" />
     <!-- Select Shipments -->
+    <div class="Delivery" style="font-weight: bold">
+      <span class="serial-number">1</span>
+      <span class="stars_red">*</span>Select Shipments<span class="title_warning"
+        >*Please select items with the same consignee.</span
+      >
+    </div>
     <div class="select_shipments">
       <div v-if="isNotSameConfiguration" class="alertIndormation">
         Please select same consignee with same delivery information
       </div>
-      <div style="margin-bottom: 16px; font-weight: bold">
-        <span class="stars_red">*</span>Select Shipments<span class="title_warning"
-          >*Please select items with the same consignee.</span
-        >
-      </div>
+
       <div class="shipments_search" v-if="a == undefined">
-        <div class="left-filter-search">
+        <div class="top-filter-search">
           <el-input
             placeholder="Enter Booking/HBL/PO/Carrier Booking No. "
             v-model="CreateNewBOokingSearch"
             class="log_input"
           >
             <template #prefix>
-              <span class="iconfont_icon">
-                <svg class="iconfont icon_search" aria-hidden="true">
-                  <use xlink:href="#icon-icon_search_b"></use>
-                </svg>
-              </span>
+              <span class="font_family icon-icon_search_b"></span>
             </template>
           </el-input>
-          <div class="input-with-label" style="margin: 0 8px">
+          <div class="input-with-label">
             <!-- <AutoSelect ASPlaceholder="Input Vessel Name" :ASValue="VesselName" @changeFocus="changeFocus"></AutoSelect> -->
             <el-input
               placeholder="Shipper"
@@ -754,7 +785,7 @@ onMounted(() => {
             </el-input>
             <span v-if="showLabelShipper" class="border-label">Shipper</span>
           </div>
-          <div class="input-with-label" style="margin-right: 8px">
+          <div class="input-with-label">
             <!-- <AutoSelect ASPlaceholder="Input Vessel Name" :ASValue="VesselName" @changeFocus="changeFocus"></AutoSelect> -->
             <el-input
               placeholder="Consignee"
@@ -777,7 +808,7 @@ onMounted(() => {
             ></CalendarDate>
             <span v-if="showETAlabel" class="border-label">ETA</span>
           </div>
-          <div class="input-with-label" style="margin: 0 8px">
+          <div class="input-with-label">
             <CalendarDate
               :isNeedFooter="false"
               CalendarWidth="100%"
@@ -787,7 +818,7 @@ onMounted(() => {
             <span v-if="showataLabel" class="border-label">ATA</span>
           </div>
 
-          <div class="input-with-label" style="margin-right: 8px">
+          <div class="input-with-label">
             <!-- <AutoSelect ASPlaceholder="Input Vessel Name" :ASValue="VesselName" @changeFocus="changeFocus"></AutoSelect> -->
             <a-date-picker
               v-model:value="deliveryDate"
@@ -818,14 +849,14 @@ onMounted(() => {
             </el-input>
             <span v-if="showLabelVessel" class="border-label">Vessel Name</span>
           </div>
-        </div>
-        <div class="right-btn">
-          <el-button
-            style="width: 108px"
-            class="el-button--dark create-button"
-            @click="SearchShipment"
-            >Search</el-button
-          >
+          <div class="right-btn">
+            <el-button
+              style="width: 108px"
+              class="el-button--dark create-button"
+              @click="SearchShipment"
+              >Search</el-button
+            >
+          </div>
         </div>
       </div>
       <NewbookingTable
@@ -834,7 +865,10 @@ onMounted(() => {
       ></NewbookingTable>
     </div>
     <!-- Delivery Information -->
-    <div class="Delivery">Delivery Information</div>
+    <div class="Delivery">
+      <span class="serial-number">2</span>
+      <span>Delivery Information</span>
+    </div>
     <div class="delivery_address">
       <div class="deliverty_flex">
         <div><span class="stars_red">*</span>Delivery Address</div>
@@ -927,6 +961,7 @@ onMounted(() => {
             :format="userStore.dateFormat"
             valueFormat="YYYY.MM.DD"
             placeholder="Please Select Date"
+            class="delivery-date-picker"
           >
             <template #renderExtraFooter>
               <div class="recommended">
@@ -1014,51 +1049,71 @@ onMounted(() => {
         :rows="4"
       ></el-input>
     </div>
-    <el-dialog v-model="ManageVisible" width="640" class="ManageDialog" :show-close="false">
-      <div class="manage_empty_address" v-if="ManageAddressList.length == 0">
-        <img :src="AddNewAddress" width="48" style="margin-bottom: 8px" />
-        <el-button
-          :disabled="isNotClickAddress"
-          class="el-button--main"
-          @click="AddNewAddressDelivery"
-        >
-          + Add New Address</el-button
-        >
-      </div>
-      <el-radio-group v-model="Addressradio">
-        <el-radio v-for="(item, index) in ManageAddressList" :key="index" :value="index">
-          <div class="addressradio">
-            <div class="radio_top">
-              <div class="radio_title">{{ item.contact_person }}({{ item.contact_number }})</div>
-              <div v-if="item.create_user == 'Online_D_Address'">
-                <el-button @click="handleClickEditAddress(item)" class="el-button--blue"
-                  ><span class="font_family icon-icon_edit_b"></span
-                ></el-button>
-                <el-button @click="handleDeleteAddress(item)" class="el-button--blue"
-                  ><span class="font_family icon-icon_delete_b"></span
-                ></el-button>
-              </div>
-            </div>
-            <div class="radio_content">
-              <div v-if="item.address_1 != null && item.address_1 != ''" class="radio_content_text">
-                {{ item.address_1 }}
-              </div>
-              <div v-if="item.address_2 != null && item.address_2 != ''" class="radio_content_text">
-                {{ item.address_2 }}
-              </div>
-              <div v-if="item.address_3 != null && item.address_3 != ''" class="radio_content_text">
-                {{ item.address_3 }}
-              </div>
-              <div v-if="item.address_4 != null && item.address_4 != ''" class="radio_content_text">
-                {{ item.address_4 }}
+    <el-dialog
+      @closed="clearManageDialog"
+      v-model="ManageVisible"
+      width="640"
+      class="ManageDialog"
+      :show-close="false"
+    >
+      <div v-vloading="manageLoading">
+        <div class="manage_empty_address" v-if="ManageAddressList.length == 0">
+          <img :src="AddNewAddress" width="48" style="margin-bottom: 8px" />
+          <el-button
+            :disabled="isNotClickAddress"
+            class="el-button--main"
+            @click="AddNewAddressDelivery"
+          >
+            + Add New Address</el-button
+          >
+        </div>
+        <el-radio-group v-model="Addressradio" style="max-height: 50vh; overflow: auto">
+          <el-radio v-for="(item, index) in ManageAddressList" :key="index" :value="index">
+            <div class="addressradio">
+              <div class="radio_top">
+                <div class="radio_title">{{ item.contact_person }}({{ item.contact_number }})</div>
+                <div v-if="item.create_user == 'Online_D_Address'">
+                  <el-button @click="handleClickEditAddress(item)" class="el-button--blue"
+                    ><span class="font_family icon-icon_edit_b"></span
+                  ></el-button>
+                  <el-button @click="handleDeleteAddress(item)" class="el-button--blue"
+                    ><span class="font_family icon-icon_delete_b"></span
+                  ></el-button>
+                </div>
               </div>
-              <div class="radio_content_text">
-                {{ item.country }},{{ item.city }},{{ item.postal_code }}
+              <div class="radio_content">
+                <div
+                  v-if="item.address_1 != null && item.address_1 != ''"
+                  class="radio_content_text"
+                >
+                  {{ item.address_1 }}
+                </div>
+                <div
+                  v-if="item.address_2 != null && item.address_2 != ''"
+                  class="radio_content_text"
+                >
+                  {{ item.address_2 }}
+                </div>
+                <div
+                  v-if="item.address_3 != null && item.address_3 != ''"
+                  class="radio_content_text"
+                >
+                  {{ item.address_3 }}
+                </div>
+                <div
+                  v-if="item.address_4 != null && item.address_4 != ''"
+                  class="radio_content_text"
+                >
+                  {{ item.address_4 }}
+                </div>
+                <div class="radio_content_text">
+                  {{ item.country }},{{ item.city }},{{ item.postal_code }}
+                </div>
               </div>
             </div>
-          </div>
-        </el-radio>
-      </el-radio-group>
+          </el-radio>
+        </el-radio-group>
+      </div>
       <template #header>
         <div class="my-header">
           <div class="header_Title">Manage Address</div>
@@ -1316,12 +1371,8 @@ onMounted(() => {
         </div>
       </template>
       <template #header>
-        <div class="cancel_header">
-          <span class="iconfont_icon">
-            <svg class="iconfont icon_danger" aria-hidden="true">
-              <use xlink:href="#icon-icon_fail_fill_b"></use>
-            </svg>
-          </span>
+        <div class="unable-save-header dialog-header">
+          <span class="font_family icon-icon_fail_fill_b"></span>
           Unable to Save
         </div>
       </template>
@@ -1341,12 +1392,8 @@ onMounted(() => {
         </div>
       </template>
       <template #header>
-        <div class="cancel_header">
-          <span class="iconfont_icon iconfont_warning">
-            <svg class="iconfont icon_warning" aria-hidden="true">
-              <use xlink:href="#icon-icon_tipsfilled_b"></use>
-            </svg>
-          </span>
+        <div class="warning-header dialog-header">
+          <span class="font_family icon-icon_fail_fill_b"></span>
           Unsaved Changes
         </div>
       </template>
@@ -1372,12 +1419,8 @@ onMounted(() => {
         </div>
       </template>
       <template #header>
-        <div class="cancel_header">
-          <span class="iconfont_icon">
-            <svg class="iconfont iconfont_warning" aria-hidden="true">
-              <use xlink:href="#icon-icon_tipsfilled_b"></use>
-            </svg>
-          </span>
+        <div class="warning-header dialog-header">
+          <span class="font_family icon-icon_warning_fill_b"></span>
           Additional Fees Notice
         </div>
       </template>
@@ -1403,6 +1446,10 @@ onMounted(() => {
   background-color: var(--color-mode);
   box-sizing: border-box;
 }
+.select-info {
+  font-size: 14px;
+  line-height: 40px;
+}
 .flex {
   display: flex;
 }
@@ -1421,7 +1468,7 @@ onMounted(() => {
 }
 .select_shipments {
   border: 1px solid var(--color-border);
-  margin: 16px 24px 12px 24px;
+  margin: 10px 24px 12px 24px;
   padding: 9px 16px 16px 16px;
   border-radius: 12px;
   position: relative;
@@ -1456,10 +1503,10 @@ onMounted(() => {
   display: flex;
   margin-bottom: 16px;
 }
-.left-filter-search {
+.top-filter-search {
   flex: 1;
   display: grid;
-  grid-template-columns: repeat(3, 1fr);
+  grid-template-columns: repeat(4, 1fr);
   grid-gap: 8px;
 }
 :deep(.log_input .el-input__wrapper) {
@@ -1473,7 +1520,7 @@ onMounted(() => {
   position: absolute;
   top: -7px;
   left: 10px;
-  background: white; /* 用背景色覆盖边框 */
+  background: var(--color-mode); /* 用背景色覆盖边框 */
   padding: 0 5px;
   font-size: 12px;
   color: var(--color-neutral-2);
@@ -1487,9 +1534,22 @@ onMounted(() => {
   height: 40px;
 }
 .Delivery {
+  display: flex;
+  align-items: center;
   font-size: 18px;
   font-weight: 700;
-  margin: 11px 0 14px 24px;
+  line-height: 24px;
+  margin: 24px 0 10px 24px;
+  .serial-number {
+    width: 24px;
+    height: 24px;
+    margin-right: 4px;
+    background-color: var(--color-theme);
+    text-align: center;
+    color: #fff;
+    font-size: 14px;
+    border-radius: 50%;
+  }
 }
 .empty_address {
   height: 122px;
@@ -1518,7 +1578,7 @@ onMounted(() => {
   margin-bottom: 4px;
 }
 .recommended {
-  background: #f5f7fa;
+  background: var(--color-personal-preference-bg);
   border-bottom: 1px solid var(--color-border);
   height: 24px;
   display: flex;
@@ -1580,7 +1640,7 @@ onMounted(() => {
   font-size: 18px;
 }
 .addressradio {
-  background-color: #f8f9fd;
+  background-color: var(--color-prompt-diaolog-bg);
   border-radius: 12px;
   padding: 13px 8px 16px 16px;
 }
@@ -1726,4 +1786,29 @@ onMounted(() => {
 :deep(header.el-dialog__header) {
   background-color: var(--color-system-body-bg);
 }
+.dialog-header {
+  display: flex;
+  align-items: center;
+  .font_family {
+    font-size: 14px;
+    width: 16px;
+    height: 16px;
+    border-radius: 24px;
+    margin-right: 4px;
+  }
+}
+.unable-save-header .font_family {
+  color: #b53039;
+}
+
+div.warning-header .font_family {
+  color: #e9b227;
+}
+</style>
+<style lang="scss">
+.ant-picker-dropdown .ant-picker-cell .ant-picker-cell-inner {
+  height: 32px;
+  width: 32px;
+  margin: 0 auto;
+}
 </style>

+ 34 - 13
src/views/DestinationDelivery/src/components/CreateNewBooking/src/components/NewbookingTable.vue

@@ -31,7 +31,7 @@ const tableData = ref<VxeGridProps<any>>({
     backgroundColor: 'var(--color-table-header-bg)'
   },
   columnConfig: { resizable: true, useKey: true },
-  rowConfig: { isHover: true, isCurrent: true }
+  rowConfig: { isHover: true }
 })
 
 const tableRef = ref<VxeGridInstance | null>(null)
@@ -58,8 +58,9 @@ const handleColumns = (columns: any) => {
       curColumn = {
         ...curColumn,
         formatter: ({ cellValue }: any) => {
+          if (!cellValue) return '--'
           const array = cellValue.split('-')
-          return `${formatTimezone(array[0])} - ${formatTimezone(array[1])}`
+          return `${formatTimezone(array[0])} -- ${formatTimezone(array[1])}`
         }
       }
     } else if (item.type === 'download') {
@@ -110,11 +111,11 @@ const searchTableData = (val: any) => {
     })
     .then((res: any) => {
       if (res.code === 200) {
-        tableLoadingTable.value = false
         tableData.value.data = res.data.data
       }
     })
     .finally(() => {
+      tableLoadingTable.value = false
       nextTick(() => {
         tableRef.value &&
           autoWidth(tableData.value, tableRef.value, {
@@ -309,22 +310,34 @@ useRowClickStyle(tableRef)
 onMounted(() => {
   getTableColumns()
 })
+const getTableCheckedRows = () => {
+  const $grid = tableRef.value
+  if ($grid) {
+    const records = $grid.getCheckboxRecords()
+    return records
+  }
+}
 defineExpose({
   getTableData,
-  searchTableData
+  searchTableData,
+  getTableCheckedRows,
+  tableLoadingTable
 })
 </script>
 
 <template>
   <div class="new-booking-table">
-    <el-button
-      style="width: 170px; align-self: flex-end"
-      type="default"
-      @click="handleCustomizeColumns"
-    >
-      <span style="margin-right: 6px" class="font_family icon-icon_column_b"></span>
-      Customize Columns
-    </el-button>
+    <div class="table-tools">
+      <div class="table-total-info">{{ tableData.data.length }} result</div>
+      <el-button
+        style="width: 170px; align-self: flex-end"
+        type="default"
+        @click="handleCustomizeColumns"
+      >
+        <span style="margin-right: 6px" class="font_family icon-icon_column_b"></span>
+        Customize Columns
+      </el-button>
+    </div>
     <vxe-grid
       ref="tableRef"
       :style="{ border: 'none' }"
@@ -335,7 +348,7 @@ defineExpose({
     >
       <!-- download下载的插槽 -->
       <template #download="{ row, column }">
-        <div class="download-btn" @click="handleDownload(row.serial_no, column.field)">
+        <div class="download-btn" @click="handleDownload(row.h_serial_no, column.field)">
           <span class="font_family icon-icon_download_b icon-style"> </span>
           <span
             >{{ row.h_bol
@@ -398,5 +411,13 @@ defineExpose({
   display: flex;
   flex-direction: column;
   gap: 8px;
+  .table-tools {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    .table-total-info {
+      color: var(--color-neutral-2);
+    }
+  }
 }
 </style>

+ 1 - 1
src/views/DestinationDelivery/src/components/DeliveryDate.vue

@@ -252,7 +252,7 @@ div.delivery-date-range-picker {
     }
 
     &:not(.ant-picker-cell-range-end):hover {
-      background-color: var(--border-hover-color);
+      background-color: var(--border-hover-color) !important;
       border-radius: 12px;
     }
     &.ant-picker-cell-selected.ant-picker-cell-in-view,

+ 294 - 0
src/views/DestinationDelivery/src/components/ListView.vue

@@ -0,0 +1,294 @@
+<script setup lang="ts">
+import { useCalculatingHeight } from '@/hooks/calculatingHeight'
+import TableView from './TableView'
+import dayjs from 'dayjs'
+import DeliveryDate from './DeliveryDate.vue'
+
+const filterRef: Ref<HTMLElement | null> = ref(null)
+const containerHeight = useCalculatingHeight(document.documentElement, 456, [filterRef])
+
+const queryData = ref({
+  text_search: '',
+  created_time_start: '',
+  created_time_end: '',
+  delivery_mode: '',
+  delivery_date_start: '',
+  delivery_date_end: '',
+  filterTag: ['ALL']
+})
+const isEmployeeRole = computed(() => {
+  return tableRef.value?.isEmployeeRole
+})
+
+const modeList = [
+  {
+    label: 'Truck',
+    value: 'Truck'
+  },
+  {
+    label: 'Rail',
+    value: 'Rail'
+  }
+]
+
+const numberCards = ref([
+  {
+    label: 'Total Bookings',
+    key: 'ALL',
+    value: 0,
+    color: '#2b2f36'
+  },
+  {
+    label: 'Pending Approval',
+    value: 0,
+    color: '#edb82f',
+    icon: 'icon_time_b'
+  },
+  {
+    label: 'Approved',
+    value: 0,
+    color: '#00a870',
+    icon: 'icon_confirm_b'
+  },
+  {
+    label: 'Rejected',
+    value: 0,
+    color: '#c9353f',
+    icon: 'icon_reject_b'
+  },
+  {
+    label: 'Cancelled',
+    value: 0,
+    color: '#243041',
+    icon: 'icon_cancelled_b'
+  }
+])
+const clickCard = (filterTagItem: string) => {
+  queryData.value.filterTag.pop()
+  queryData.value.filterTag.push(filterTagItem)
+  handleSearch()
+}
+
+const DateChange = (date: any) => {
+  queryData.value.created_time_start = date ? date[0] : ''
+  queryData.value.created_time_end = date ? date[1] : ''
+}
+const deliveryDataChange = (date) => {
+  queryData.value.delivery_date_start = date ? date[0] : ''
+  queryData.value.delivery_date_end = date ? date[1] : ''
+}
+
+const DateStart = ref([])
+const tableRef = ref()
+
+const setNumberCards = (cards) => {
+  numberCards.value = cards
+}
+
+const handleSearch = () => {
+  tableRef.value.searchTableData()
+}
+
+const searchTableData = (date: string) => {
+  if (!date) {
+    handleSearch()
+    return
+  }
+  DateStart.value = [date, date]
+  queryData.value.delivery_date_start = dayjs(date).format('MM/DD/YYYY')
+  queryData.value.delivery_date_end = dayjs(date).format('MM/DD/YYYY')
+  handleSearch()
+}
+
+defineExpose({
+  isEmployeeRole,
+  searchTableData
+})
+</script>
+
+<template>
+  <div class="display">
+    <div class="header_top">
+      <div class="date-tips_filter filter-item">
+        <span class="label">Delivery Date:</span>
+        <DeliveryDate @DateChange="deliveryDataChange" :Date="DateStart" />
+      </div>
+
+      <div class="tips_filter filter-item">
+        <span class="label">Delivery Mode:</span>
+        <el-select v-model="queryData.delivery_mode" placeholder="" clearable>
+          <el-option
+            v-for="item in modeList"
+            :key="item.value"
+            :label="item.label"
+            :value="item.value"
+          />
+        </el-select>
+      </div>
+      <div class="date-tips_filter filter-item">
+        <span class="label">Creation Date</span>
+        <CalendarDate :isShowPopupClass="true" @DateChange="DateChange"></CalendarDate>
+      </div>
+      <div class="input-tips_filter filter-item">
+        <el-input
+          placeholder="Search Question Booking No、HBOL No、MBL No、Container No、Consignee"
+          v-model="queryData.text_search"
+          class="log_input"
+        >
+          <template #prefix>
+            <span class="iconfont_icon">
+              <svg class="iconfont icon_dark" aria-hidden="true">
+                <use xlink:href="#icon-icon_search_b"></use>
+              </svg>
+            </span>
+          </template>
+        </el-input>
+      </div>
+      <el-button class="el-button--dark" @click="handleSearch">Search</el-button>
+    </div>
+    <div class="number-cards">
+      <div
+        class="card"
+        :class="{
+          'is-active':
+            queryData.filterTag.includes(item.key as string | undefined) ||
+            queryData.filterTag.includes(item.label)
+        }"
+        @click="clickCard((item.key || item.label) as string)"
+        v-for="(item, index) in numberCards"
+        :key="index"
+      >
+        <div class="card-label">{{ item.label }}</div>
+        <div
+          class="card-value"
+          :style="{
+            color: item.label === 'Cancelled' ? `var(--color-card-number-cancelled)` : item.color
+          }"
+        >
+          {{ item.value }}
+        </div>
+        <div class="icon-box" v-if="item.icon">
+          <span class="font_family" :class="'icon-' + item.icon"></span>
+        </div>
+      </div>
+    </div>
+  </div>
+  <TableView
+    @get-number-cards="setNumberCards"
+    :height="containerHeight"
+    :queryData="queryData"
+    ref="tableRef"
+  ></TableView>
+</template>
+
+<style lang="scss" scoped>
+.header_top {
+  margin-bottom: 8px;
+  padding-right: 8px;
+  display: flex;
+  align-items: flex-end;
+}
+.number-cards {
+  display: flex;
+  justify-content: space-between;
+  margin: 8px 0;
+  gap: 8px;
+  .card {
+    position: relative;
+    flex: 1;
+    height: 80px;
+    padding: 12px 16px;
+    background-color: var(--color-email-bg);
+    border-radius: 12px;
+    & > .icon-box {
+      position: absolute;
+      right: 16px;
+      top: 12px;
+      width: 24px;
+      height: 24px;
+      border-radius: 6px;
+      background-color: var(--color-card-icon-box-bg);
+      text-align: center;
+      line-height: 24px;
+    }
+    &.is-active {
+      background-color: var(--color-mune-active-bg);
+      .card-label,
+      .card-value {
+        color: var(--color-theme) !important;
+      }
+      .icon-box {
+        background-color: rgba(#ff7500, 0.1);
+        span {
+          color: var(--color-theme);
+        }
+      }
+    }
+
+    .card-value {
+      margin-top: 4px;
+      font-size: 32px;
+      font-weight: 700;
+    }
+  }
+}
+
+.display {
+  border: 1px solid var(--color-border);
+  border-width: 0 0 1px 0;
+  padding: 0 24px;
+}
+:deep(.el-select__placeholder.is-transparent span) {
+  color: var(--tag-info-text-color) !important;
+}
+:deep(.ETD_title) {
+  margin-bottom: 0;
+}
+:deep(.ant-picker-range) {
+  width: 250px !important;
+  height: 32px;
+  background-color: var(--color-mode) !important;
+}
+.filter-item {
+  flex: 1;
+  display: flex;
+  flex-direction: column;
+  align-items: flex-end;
+  justify-content: flex-end;
+  margin-right: 8px;
+  .label {
+    align-self: flex-start;
+    font-size: 12px;
+    margin-bottom: 3px;
+  }
+}
+.tips_filter {
+  min-width: 170px;
+  max-width: 220px;
+}
+.input-tips_filter {
+  max-width: 540px;
+  :deep(.el-input__wrapper) {
+    height: 32px;
+    input {
+      width: 100%;
+      white-space: nowrap; /* 防止换行 */
+      overflow: hidden; /* 超出部分隐藏 */
+      text-overflow: ellipsis; /* 超出部分显示为省略号 */
+    }
+  }
+}
+.date-tips_filter {
+  max-width: 250px;
+  height: 56px;
+}
+
+.destination-delivery {
+  position: relative;
+  background-color: var(--color-mode);
+}
+:deep(.log_input .el-input__wrapper) {
+  box-shadow: 0 0 0 1px var(--color-select-border);
+  border-radius: 6px;
+}
+</style>

+ 0 - 1
src/views/DestinationDelivery/src/components/ModifyBooking/index.ts

@@ -1 +0,0 @@
-export { default } from './src/ModifyBooking.vue'

+ 0 - 275
src/views/DestinationDelivery/src/components/ModifyBooking/src/ModifyBooking.vue

@@ -1,275 +0,0 @@
-<script lang="ts" setup>
-import SelectShipmentsTable from './components/SelectShipmentsTable.vue'
-import { useRouter } from 'vue-router'
-
-const router = useRouter()
-
-const selectVModel = ref('')
-const optionOptions = ref([
-  { key: '1', value: 'option1', label: 'Option 1' },
-  { key: '2', value: 'option2', label: 'Option 2' },
-  { key: '3', value: 'option3', label: 'Option 3' }
-])
-
-const inputVModel = ref('')
-const radioVModel = ref(1)
-const deliveryDate = ref('')
-
-const handleCancel = () => {
-  // Logic to handle cancel action
-  router.push({ name: 'Destination Delivery' })
-}
-</script>
-<template>
-  <div class="modify-booking">
-    <div class="header">
-      <span>Modify Booking</span>
-      <div class="operator">
-        <el-button @click="handleCancel" style="height: 40px; width: 115px" type="default">
-          <span style="margin-right: 4px" class="font_family icon-icon_return_b"></span>
-          <span style="font-weight: 400">Cancel</span></el-button
-        >
-        <el-button style="height: 40px; width: 120px" class="el-button--main el-button--pain-theme">
-          <span
-            style="
-              display: inline-block;
-              margin-top: -4px;
-              margin-right: 4px;
-              transform: rotate(-60deg);
-            "
-            class="font_family icon-icon_submit_b"
-          ></span>
-          <span style="font-weight: 400">Submit</span>
-        </el-button>
-      </div>
-    </div>
-    <div class="content">
-      <div class="booking-info">
-        <div class="booking-no">
-          <span class="no">Booking No.B83131200164</span>
-          <v-tag class="tag" type="Pending Approval">Pending Approval</v-tag>
-        </div>
-      </div>
-      <el-divider style="margin: 8px 0" />
-      <SelectShipmentsTable></SelectShipmentsTable>
-      <el-divider style="margin: 8px 0 20px" />
-      <div class="delivery-information">
-        <div class="label">Delivery Information</div>
-        <div class="delivery-info-content">
-          <div class="delivery-address-header">
-            <div class="label"><span class="require-icon">*</span>Delivery Address</div>
-            <div class="operator">
-              <el-button class="el-button--text" style="height: 32px">
-                <span class="font_family icon-icon_add_b" style="margin-right: 4px"></span>
-                <span>Add New Address</span>
-              </el-button>
-              <el-button class="el-button--text" style="height: 32px">
-                <span
-                  class="font_family icon-icon_configurations_b"
-                  style="margin-right: 4px"
-                ></span>
-                <span>Manage Address</span>
-              </el-button>
-            </div>
-          </div>
-          <div class="delivery-address-content">
-            <el-radio-group v-model="radioVModel">
-              <el-radio :value="1" label="string">
-                <div class="delivery-address-label">Main Distribution Center</div>
-                <div class="delivery-address-info">
-                  <p>160#BEIJING ROAD, JINGAN District,</p>
-                  <p>Shenzhen, China</p>
-                  <p class="time">Contact: John Doe (+65 9123 4567)</p>
-                </div>
-              </el-radio>
-            </el-radio-group>
-          </div>
-          <div class="filetr-item mode-type">
-            <div class="label"><span class="require-icon">*</span>Mode Type</div>
-            <el-select v-model="selectVModel" clearable placeholder="Please Select Type">
-              <el-option
-                v-for="item in optionOptions"
-                :key="item.key"
-                :value="item.value"
-                :label="item.label"
-              ></el-option>
-            </el-select>
-          </div>
-          <div class="filetr-item mode-type">
-            <div class="label"><span class="require-icon">*</span>Preferred Delivery Date</div>
-            <el-date-picker v-model="deliveryDate" type="date" placeholder="Pick a day" />
-          </div>
-          <div class="special-requirements">
-            <div class="label">Special Requirements</div>
-            <div class="tag-list">
-              <div class="tag-item">Tail Lift Required</div>
-              <div class="tag-item">Side Loading</div>
-              <div class="tag-item">Forklift Required</div>
-              <div class="tag-item">Special Equipment</div>
-            </div>
-            <el-input
-              type="textarea"
-              class="input-textarea"
-              v-model="inputVModel"
-              placeholder="Enter any additional requirements or notes..."
-            ></el-input>
-          </div>
-          <div class="modification-reason">
-            <div class="label" style="margin-bottom: 4px">
-              <span class="require-icon">*</span>Modification Reason
-            </div>
-            <el-input class="input-textarea" type="textarea" v-model="inputVModel"></el-input>
-          </div>
-        </div>
-      </div>
-    </div>
-  </div>
-</template>
-
-<style lang="scss" scoped>
-.modify-booking {
-  position: relative;
-  background-color: var(--color-mode);
-}
-.header {
-  display: flex;
-  height: 68px;
-  border-bottom: 1px solid var(--color-border);
-  font-size: var(--font-size-6);
-  font-weight: 700;
-  padding: 0 24px;
-  align-items: center;
-  justify-content: space-between;
-}
-
-.content {
-  padding: 16px 24px 48px;
-}
-
-.booking-info {
-  display: flex;
-  align-items: center;
-  height: 48px;
-  padding: 0 16px;
-  border-radius: 12px;
-  background-image: var(--color-booking-info-linear-bg);
-
-  .booking-no {
-    display: flex;
-    align-items: center;
-    .no {
-      margin-top: 2px;
-      font-size: 18px;
-      font-weight: 700;
-      line-height: 21px;
-    }
-    .tag {
-      margin-left: 8px;
-    }
-  }
-  .created-time {
-    margin-top: 8px;
-    font-size: 12px;
-    color: var(--color-neutral-2);
-  }
-}
-
-.delivery-information {
-  & > .label {
-    font-size: 18px;
-    font-weight: 700;
-    margin-bottom: 16px;
-  }
-  .delivery-info-content {
-    padding: 16px;
-    border: 1px solid var(--color-border);
-    border-radius: 12px;
-    .label {
-      font-weight: 700;
-      line-height: 22px;
-    }
-    .delivery-address-header {
-      display: flex;
-      justify-content: space-between;
-      align-items: center;
-      margin-bottom: 4px;
-      & > .operator {
-        display: flex;
-        align-items: center;
-        & > .el-button--text {
-          height: 32px;
-          font-size: 14px;
-          font-weight: 400;
-          span {
-            color: var(--color-theme);
-          }
-        }
-      }
-    }
-    .delivery-address-content {
-      height: 122px;
-      margin-bottom: 16px;
-      padding: 10px 12px 8px 8px;
-      border-radius: 12px;
-      background-color: var(--color-share-link-bg);
-      :deep(.el-radio__label) {
-        display: flex;
-        flex-direction: column;
-        height: 100%;
-        justify-content: space-between;
-      }
-      .delivery-address-label {
-        margin-top: 9px;
-        margin-bottom: 2px;
-        font-weight: 700;
-      }
-      .delivery-address-info {
-        margin-top: 8px;
-        p {
-          line-height: 21px;
-          color: var(--color-neutral-2);
-        }
-        & > .time {
-          margin-top: 6px;
-          font-size: 12px;
-        }
-      }
-    }
-    .filetr-item {
-      display: inline-flex;
-      flex-direction: column;
-      width: 240px;
-      margin-right: 16px;
-      & > .label {
-        margin-bottom: 4px;
-      }
-    }
-    .special-requirements {
-      margin: 16px 0;
-    }
-    .input-textarea {
-      :deep(.el-textarea__inner) {
-        height: 80px;
-        resize: none;
-        line-height: 22px;
-      }
-    }
-    .tag-list {
-      margin-top: 8px;
-      margin-bottom: 8px;
-      .tag-item {
-        display: inline-block;
-        height: 32px;
-        padding: 10px 16px;
-        margin-right: 8px;
-        background-color: var(--color-download-file-filter-tag-bg);
-        border-radius: 15px;
-        font-size: 12px;
-      }
-    }
-  }
-  .require-icon {
-    color: red;
-  }
-}
-</style>

+ 0 - 152
src/views/DestinationDelivery/src/components/ModifyBooking/src/components/SelectShipmentsTable.vue

@@ -1,152 +0,0 @@
-<script setup lang="ts">
-import { type VxeGridInstance, type VxeGridProps } from 'vxe-table'
-// import { autoWidth } from '@/utils/table'
-import { useRowClickStyle } from '@/hooks/rowClickStyle'
-import { formatTimezone, formatNumber } from '@/utils/tools'
-
-const props = defineProps({
-  data: Object
-})
-const tableRef = ref<VxeGridInstance | null>(null)
-const tableData = ref<VxeGridProps<any>>({
-  minHeight: 70,
-  border: true,
-  round: true,
-  columns: [],
-  data: [],
-  scrollY: { enabled: true, oSize: 20, gt: 30 },
-  emptyText: ' ',
-  showHeaderOverflow: true,
-  showOverflow: true,
-  headerRowStyle: {
-    backgroundColor: 'var(--color-table-header-bg)'
-  },
-  columnConfig: { resizable: true, useKey: true },
-  rowConfig: { isHover: true }
-})
-
-const handleColumns = (columns: any) => {
-  const newColumns = columns.map((item: any) => {
-    let curColumn: any = {
-      title: item.title,
-      field: item.field,
-      minWidth: 30
-    }
-
-    // 格式化
-    if (item.formatter === 'date' || item.formatter === 'dateTime') {
-      curColumn = {
-        ...curColumn,
-        formatter: ({ cellValue }: any) => formatTimezone(cellValue)
-      }
-    } else if (item.formatter === 'number') {
-      curColumn = {
-        ...curColumn,
-        formatter: ({ cellValue }: any) => formatNumber(Number(cellValue), item?.digits)
-      }
-    }
-    return curColumn
-  })
-  return newColumns
-}
-watch(
-  () => props.data,
-  (newVal) => {
-    const containers = newVal?.containers
-    if (containers && containers.container_column) {
-      tableData.value.columns = handleColumns(containers.container_column)
-      tableData.value.data = containers.container_data
-      // nextTick(() => {
-      //   tableRef.value && autoWidth(tableData.value, tableRef.value)
-      // })
-    }
-  },
-  {
-    immediate: true,
-    deep: true
-  }
-)
-
-const tableLoadingColumn = ref(false)
-const tableLoadingTableData = ref(false)
-
-const tableOriginColumnsField = ref()
-// 获取表格列
-const getTableColumns = async () => {
-  tableLoadingColumn.value = true
-  await $api.getOperationTableColumns().then((res: any) => {
-    if (res.code === 200) {
-      tableData.value.columns = [
-        { type: 'checkbox', width: 50, fixed: 'left' },
-        ...handleColumns(res.data.OperationTableColumns),
-        {}
-      ]
-
-      tableOriginColumnsField.value = res.data.OperationTableColumns
-
-      tableLoadingColumn.value = false
-    }
-  })
-}
-let searchdata: any = {}
-// 获得表格数据后赋值
-const assignTableData = (data: any) => {
-  tableData.value.data = data.searchData || []
-}
-
-// 获取表格数据
-const getTableData = async () => {
-  const rc = -1
-  tableLoadingTableData.value = true
-  await $api
-    .SearchOperationLog({
-      cp: 1,
-      ps: 6,
-      rc,
-      ...searchdata
-    })
-    .then((res: any) => {
-      if (res.code === 200) {
-        assignTableData(res.data)
-      }
-    })
-    .finally(() => {
-      tableLoadingTableData.value = false
-    })
-}
-
-onMounted(() => {
-  Promise.all([getTableColumns(), getTableData()]).finally(() => {})
-})
-// 实现行点击样式
-useRowClickStyle(tableRef)
-</script>
-
-<template>
-  <div class="shipment-table">
-    <div class="label">*Select Shipments</div>
-    <vxe-grid
-      ref="tableRef"
-      v-vloading="tableLoadingTableData || tableLoadingColumn"
-      v-bind="tableData"
-      height="240"
-    >
-      <template #empty v-if="!tableLoadingTableData && tableData.data.length === 0">
-        <div class="empty">No data</div>
-      </template>
-    </vxe-grid>
-  </div>
-</template>
-
-<style lang="scss" scoped>
-.shipment-table {
-  padding: 16px;
-  border: 1px solid var(--color-border);
-  border-radius: 12px;
-  .label {
-    margin-bottom: 16px;
-    font-size: 18px;
-    font-weight: 700;
-  }
-}
-</style>

+ 5 - 17
src/views/DestinationDelivery/src/components/TableView/src/TableView.vue

@@ -179,7 +179,7 @@ const getTableData = async (isPageChange?: boolean) => {
       })
     })
 }
-const SearchOperationLog = () => {
+const searchTableData = () => {
   tableLoadingTableData.value = true
   $api
     .getDeliveryBookingTableData({
@@ -204,7 +204,7 @@ const SearchOperationLog = () => {
     })
 }
 onMounted(() => {
-  Promise.all([getTableColumns(), getTableData(false)]).finally(() => {
+  Promise.all([getTableColumns()]).finally(() => {
     nextTick(() => {
       tableRef.value && autoWidth(tableData.value, tableRef.value)
     })
@@ -273,7 +273,7 @@ const tableData = ref<VxeGridProps<any>>({
     }
   },
   columnConfig: { resizable: true, useKey: true },
-  rowConfig: { isHover: true, isCurrent: true },
+  rowConfig: { isHover: true },
   exportConfig: {
     types: ['csv', 'html', 'txt', 'xlsx'],
     modes: ['current', 'selected', 'all']
@@ -310,17 +310,6 @@ const handleCreate = () => {
   router.push({ name: 'Create New Booking' })
 }
 
-const testData = [
-  {
-    key: 'A1703530062',
-    value: 'CD1Xeh4rG%2FPmYIiCHAw2%2B0Gw8BFZ8k%2F2pQDkYX3vuf6iZ3kcQXj%2BzQ'
-  },
-  {
-    key: 'A1703530062',
-    value: 'CD1Xeh4rG%2FPmYIiCHAw2%2B0Gw8BFZ8k%2F2pQDkYX3vuf6iZ3kcQXj%2BzQ'
-  }
-]
-
 const handleMultLinkClick = (item: any) => {
   router.push({
     path: '/tracking/detail',
@@ -333,11 +322,11 @@ const handleTips = (type: string, row: any) => {
   tipsDialogRef.value.openDialog(type, row)
 }
 const handleChangeRowState = () => {
-  SearchOperationLog()
+  searchTableData()
 }
 
 defineExpose({
-  SearchOperationLog,
+  searchTableData,
   isEmployeeRole
 })
 </script>
@@ -357,7 +346,6 @@ defineExpose({
       ref="tableRef"
       v-vloading="tableLoadingTableData || tableLoadingColumn"
       :height="props.height"
-      @cell-dblclick="({ row }) => handleEdit(row)"
       :style="{ border: 'none' }"
       v-bind="tableData"
     >

+ 112 - 48
src/views/DestinationDelivery/src/components/TableView/src/components/ShipmentInforTable.vue

@@ -2,6 +2,7 @@
 import { type VxeGridInstance, type VxeGridProps } from 'vxe-table'
 import { useRowClickStyle } from '@/hooks/rowClickStyle'
 import { formatTimezone, formatNumber } from '@/utils/tools'
+import { autoWidth } from '@/utils/table'
 
 const props = defineProps({
   data: Object
@@ -11,40 +12,7 @@ const tableData = ref<VxeGridProps<any>>({
   minHeight: 70,
   border: true,
   round: true,
-  columns: [
-    {
-      field: 'HBOL No.',
-      title: 'HBOL No.',
-      width: 120
-    },
-    {
-      field: 'Container No.',
-      title: 'Container No.'
-    },
-    {
-      field: 'Service Type',
-      title: 'Service Type'
-    },
-    {
-      field: 'ETA',
-      title: 'ETA',
-      minWidth: 100,
-      formatter: ({ cellValue }: any) => formatTimezone(cellValue)
-    },
-
-    {
-      field: 'Recommended Delivery Date',
-      title: 'Recommended Delivery Date',
-      width: 220,
-      formatter: ({ cellValue }: any) => {
-        if (!cellValue) return '--'
-        const rangeData = cellValue.split(';')
-        const leftDate = rangeData[0] ? formatTimezone(rangeData[0]) : '_'
-        const rightDate = rangeData[1] ? formatTimezone(rangeData[0]) : '_'
-        return leftDate + ' -- ' + rightDate
-      }
-    }
-  ],
+  columns: [],
   data: [],
   scrollY: { enabled: true, oSize: 20, gt: 30 },
   emptyText: ' ',
@@ -56,10 +24,74 @@ const tableData = ref<VxeGridProps<any>>({
   columnConfig: { resizable: true, useKey: true },
   rowConfig: { isHover: true }
 })
+const tableLoadingColumn = ref()
+
+const handleColumns = (columns: any) => {
+  const newColumns = columns.map((item: any) => {
+    let curColumn: any = {
+      title: item.title,
+      field: item.field,
+      width: '150px'
+    }
+    // 格式化
+    if (item.formatter === 'date') {
+      curColumn = {
+        ...curColumn,
+        formatter: ({ cellValue }: any) => formatTimezone(cellValue)
+      }
+    } else if (item.type === 'link') {
+      curColumn = {
+        ...curColumn,
+        slots: { default: 'trackingNo' }
+      }
+    } else if (item.type == 'recommend') {
+      curColumn = {
+        ...curColumn,
+        formatter: ({ cellValue }: any) => {
+          if (!cellValue) return '--'
+          const array = cellValue.split('-')
+          return `${formatTimezone(array[0])} -- ${formatTimezone(array[1])}`
+        }
+      }
+    } else if (item.type === 'download') {
+      curColumn = {
+        ...curColumn,
+        slots: { default: 'download' }
+      }
+    }
+    return curColumn
+  })
+  return newColumns
+}
+// 获取表格列
+const getTableColumns = async () => {
+  tableLoadingColumn.value = true
+  await $api.BookingTableColumn().then((res: any) => {
+    if (res.code === 200) {
+      tableData.value.columns = [...handleColumns(res.data.TrackingTableColumns)]
+    }
+  })
+  nextTick(() => {
+    tableLoadingColumn.value = false
+    tableRef.value &&
+      autoWidth(tableData.value, tableRef.value, {
+        packing_list: 190,
+        commercial_invoice: 180
+      })
+  })
+}
+onMounted(() => {
+  getTableColumns()
+})
 
 // 获得表格数据后赋值
 const assignTableData = (data: any) => {
   tableData.value.data = data || []
+  tableRef.value &&
+    autoWidth(tableData.value, tableRef.value, {
+      packing_list: 190,
+      commercial_invoice: 180
+    })
 }
 watch(
   () => props.data,
@@ -74,25 +106,48 @@ const tableLoadingTable = ref()
 
 let searchdata: any = {}
 
-// 获取表格数据
-const getTableData = async () => {
-  const rc = -1
-  tableLoadingTable.value = true
-  await $api
-    .SearchOperationLog({
-      cp: 1,
-      ps: 6,
-      rc,
-      ...searchdata
+function base64ToBlob(base64, mimeType) {
+  const byteString = atob(base64.split(',')[0].startsWith('data:') ? base64.split(',')[1] : base64)
+  const ab = new ArrayBuffer(byteString.length)
+  const ia = new Uint8Array(ab)
+  for (let i = 0; i < byteString.length; i++) {
+    ia[i] = byteString.charCodeAt(i)
+  }
+  return new Blob([ia], { type: mimeType })
+}
+
+const handleDownload = (serialNo: string, field: string) => {
+  const fileType = field === 'commercial_invoice' ? 'C/I' : 'Packing List'
+  $api
+    .downloadBookingTableFile({
+      serial_no: serialNo,
+      file_type: fileType
     })
     .then((res: any) => {
       if (res.code === 200) {
-        assignTableData(res.data)
+        // 使用
+        const base64 = res.data.data // 纯 base64 字符串,不含 data: 前缀
+        const mimeType = 'application/octet-stream'
+        const fileName = res.data.filename || 'download'
+
+        const blob = base64ToBlob(base64, mimeType)
+        const url = URL.createObjectURL(blob)
+
+        const link = document.createElement('a')
+        link.href = url
+        link.download = fileName
+        document.body.appendChild(link)
+        link.click()
+        document.body.removeChild(link)
+
+        // 可选:下载完成后释放内存
+        link.onclick = () => {
+          setTimeout(() => {
+            URL.revokeObjectURL(url) // 释放对象 URL
+          }, 100)
+        }
       }
     })
-    .finally(() => {
-      tableLoadingTable.value = false
-    })
 }
 
 // 实现行点击样式
@@ -109,6 +164,15 @@ useRowClickStyle(tableRef)
       max-height="240"
       min-height="120"
     >
+      <template #download="{ row, column }">
+        <div class="download-btn" @click="handleDownload(row.h_serial_no, column.field)">
+          <span class="font_family icon-icon_download_b icon-style"> </span>
+          <span
+            >{{ row.h_bol
+            }}{{ column.field === 'commercial_invoice' ? '.CI.zip' : '._PL.zip' }}</span
+          >
+        </div>
+      </template>
       <template #empty v-if="!tableLoadingTable && tableData.data.length === 0">
         <div class="empty">No data</div>
       </template>

+ 16 - 30
src/views/Layout/src/components/Header/HeaderView.vue

@@ -6,11 +6,11 @@ import { useRouter, useRoute } from 'vue-router'
 import { useUserStore } from '@/stores/modules/user'
 import { useHeaderSearch } from '@/stores/modules/headerSearch'
 import { onBeforeRouteUpdate } from 'vue-router'
-import { useLoadingState } from '@/stores/modules/loadingState'
 import { useThemeStore } from '@/stores/modules/theme'
 import { useNotificationMessage } from '@/stores/modules/notificationMessage'
 import NotificationDrawer from './components/NotificationDrawer.vue'
 import TrainingCard from './components/TrainingCard.vue'
+import KAMMapping from './components/KAMMapping.vue'
 import emitter from '@/utils/bus'
 
 const notificationMsgStore = useNotificationMessage()
@@ -29,7 +29,7 @@ const toggleThemeMode = (theme: string) => {
 }
 
 const searchValue = ref('')
-// 用于判断是否在搜索后跳转页面,跳转后清空搜索框的值
+// 用于判断是否在搜索后跳转页面,跳转后清空搜索框的值  false不用清空,true清空
 const isJumpPageBySearch = ref(false)
 const handleSearch = () => {
   if (!searchValue.value) {
@@ -69,34 +69,19 @@ const handleSearch = () => {
       }
     })
   } else {
-    const trackingTableLoadingState = useLoadingState()
-    trackingTableLoadingState.setTrackingTableLoading(true)
-    // 已登录
-    $api
-      .getTrackingTableData({
-        _textSearch: searchValue.value
-      })
-      .then((res) => {
-        if (res.code === 200) {
-          // 如果是在tracking页面搜索,那么isJumpPageBySearch不用置为true,跳转路由后直接清空搜索框
-          if (route.path === '/tracking') {
-            isJumpPageBySearch.value = false
-          } else {
-            isJumpPageBySearch.value = true
-          }
-          headerSearch.setSearchData({
-            searchValue: searchValue.value,
-            searchResult: '',
-            trackingData: res.data
-          })
-          router.push({
-            name: 'Tracking'
-          })
-        }
-      })
-      .finally(() => {
-        trackingTableLoadingState.setTrackingTableLoading(false)
-      })
+    headerSearch.setSearchData({
+      searchValue: searchValue.value,
+      searchResult: '',
+      trackingData: []
+    })
+    if (route.path === '/tracking') {
+      isJumpPageBySearch.value = false
+    } else {
+      isJumpPageBySearch.value = true
+    }
+    router.push({
+      name: 'Tracking'
+    })
   }
 }
 onBeforeRouteUpdate((to, from, next) => {
@@ -249,6 +234,7 @@ const handleDemoVideo = () => {
           <span style="margin-top: -1px" class="font_family icon-icon_search_b"></span>
         </template>
       </el-input>
+      <KAMMapping></KAMMapping>
       <div class="notice-icon" v-if="userStore.isLogin">
         <span v-if="notificationMsgStore.hasNewMsg" class="unread-tip-icon"></span>
         <el-button

+ 113 - 0
src/views/Layout/src/components/Header/components/KAMMapping.vue

@@ -0,0 +1,113 @@
+<script setup lang="ts">
+const customer = ref('')
+const isShowMapping = ref(true)
+const visible = ref(false)
+const myPopover = ref()
+
+const customerList = ref([
+  'alice@dummy customer.com',
+  'bob@dummy customer.com',
+  'charlie@dummy customer.com',
+  'dave@dummy customer.com'
+])
+
+const changeShowMapping = () => {
+  customer.value = ''
+}
+const changeCustomer = (cust: string) => {
+  customer.value = cust
+  myPopover.value?.hide()
+}
+</script>
+
+<template>
+  <div
+    class="kam-mapping-container"
+    :class="{ focus: visible }"
+    :style="{ 'justify-content': !isShowMapping ? 'center' : 'space-between' }"
+  >
+    <span style="margin-right: 5px" v-if="!isShowMapping">View as Customer</span>
+
+    <el-popover
+      @show="visible = true"
+      @hide="visible = false"
+      v-else
+      ref="myPopover"
+      trigger="click"
+      placement="bottom-start"
+      width="400px"
+      :hide-after="0"
+      popper-style="border-radius: 12px"
+    >
+      <template #reference>
+        <div class="select-customer">
+          <span
+            :style="{ color: customer ? 'var(--color-neutral-1)' : 'var(--color-neutral-3)' }"
+            >{{ customer || 'Select customer' }}</span
+          >
+          <span class="font_family icon-icon_dropdown_b"></span></div
+      ></template>
+      <div style="padding: 8px">
+        <el-input class="customer-input" placeholder="Search customer">
+          <template #suffix>
+            <span class="font_family icon-icon_search_b"></span>
+          </template>
+        </el-input>
+        <div class="select-customer-list">
+          <div
+            class="customer-item"
+            v-for="cust in customerList"
+            :key="cust"
+            @click="changeCustomer(cust)"
+          >
+            {{ cust }}
+          </div>
+        </div>
+      </div>
+    </el-popover>
+    <el-switch v-model="isShowMapping" @change="changeShowMapping" />
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.kam-mapping-container {
+  display: flex;
+  align-items: center;
+  width: 280px;
+  height: 32px;
+  margin-left: 20px;
+  padding: 0 8px;
+  border-radius: 32px;
+  line-height: 32px;
+  border: 1px solid var(--color-email-border);
+  background-color: var(--color-table-header-bg);
+  &.focus {
+    background-color: var(--color-mode) !important;
+    border-color: var(--color-theme) !important;
+  }
+}
+.select-customer {
+  cursor: pointer;
+}
+.customer-input {
+  height: 32px;
+  padding: 0 8px;
+  margin-bottom: 8px;
+  :deep(.el-input__wrapper) {
+    border-radius: 6px;
+  }
+}
+.select-customer-list {
+  height: 160px;
+  .customer-item {
+    height: 40px;
+    padding: 0 8px;
+    line-height: 40px;
+    &:hover {
+      background-color: var(--color-mune-active-bg);
+      border-radius: 6px;
+      cursor: pointer;
+    }
+  }
+}
+</style>

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

@@ -14,7 +14,7 @@ const handleLogout = () => {
   router.push('/login')
   sessionStorage.clear()
   localStorage.removeItem('user_type')
-  emitter.emit('login-out');
+  emitter.emit('login-out')
 }
 defineExpose({
   openDialog

+ 81 - 82
src/views/Layout/src/components/Menu/MenuView.vue

@@ -23,87 +23,87 @@ const getMenuList = () => {
       menuList.value = res.data
     }
   })
-  menuList.value = [
-    {
-      index: '1',
-      label: 'Dashboard',
-      icon: 'icon_data_fill_b',
-      path: '/dashboard'
-    },
-    {
-      index: '2',
-      label: 'Booking',
-      icon: 'icon_booking__fill_b',
-      type: 'list',
-      children: [
-        {
-          index: '2-1',
-          label: 'Booking Management',
-          path: '/booking'
-        },
-        {
-          index: '2-2',
-          label: 'Destination Delivery',
-          path: '/destination-delivery'
-        }
-      ]
-    },
-    {
-      index: '3',
-      label: 'Tracking',
-      icon: 'icon_tracking__fill_b',
-      path: '/tracking'
-    },
-    {
-      index: '4',
-      label: 'Report',
-      icon: 'icon_report__fill_b',
-      path: '/report'
-    },
-    {
-      index: '5',
-      label: 'System Management',
-      icon: 'icon_system__management_fill_b',
-      type: 'list',
-      children: [
-        {
-          index: '5-7',
-          label: 'Template Management',
-          path: '/template-management'
-        },
-        {
-          index: '5-1',
-          label: 'System Message',
-          path: '/system-message'
-        },
-        {
-          index: '5-2',
-          label: 'System Settings',
-          path: '/system-settings'
-        },
-        {
-          index: '5-3',
-          label: 'Chat Log',
-          path: '/chat-log'
-        },
-        {
-          index: '5-4',
-          label: 'AI API Log',
-          path: '/ai-api-log'
-        },
-        {
-          index: '5-5',
-          label: 'Operation Log',
-          path: '/operation-log'
-        },
-        {
-          index: '5-6',
-          label: 'Prompt Configuration',
-          path: '/prompt-configuration'
-        }
-      ]
-    }
-  ]
+  // menuList.value = [
+  //   {
+  //     index: '1',
+  //     label: 'Dashboard',
+  //     icon: 'icon_data_fill_b',
+  //     path: '/dashboard'
+  //   },
+  //   {
+  //     index: '2',
+  //     label: 'Booking',
+  //     icon: 'icon_booking__fill_b',
+  //     type: 'list',
+  //     children: [
+  //       {
+  //         index: '2-1',
+  //         label: 'Booking Management',
+  //         path: '/booking'
+  //       },
+  //       {
+  //         index: '2-2',
+  //         label: 'Destination Delivery',
+  //         path: '/destination-delivery'
+  //       }
+  //     ]
+  //   },
+  //   {
+  //     index: '3',
+  //     label: 'Tracking',
+  //     icon: 'icon_tracking__fill_b',
+  //     path: '/tracking'
+  //   },
+  //   {
+  //     index: '4',
+  //     label: 'Report',
+  //     icon: 'icon_report__fill_b',
+  //     path: '/report'
+  //   },
+  //   {
+  //     index: '5',
+  //     label: 'System Management',
+  //     icon: 'icon_system__management_fill_b',
+  //     type: 'list',
+  //     children: [
+  //       {
+  //         index: '5-7',
+  //         label: 'Template Management',
+  //         path: '/template-management'
+  //       },
+  //       {
+  //         index: '5-1',
+  //         label: 'System Message',
+  //         path: '/system-message'
+  //       },
+  //       {
+  //         index: '5-2',
+  //         label: 'System Settings',
+  //         path: '/system-settings'
+  //       },
+  //       {
+  //         index: '5-3',
+  //         label: 'Chat Log',
+  //         path: '/chat-log'
+  //       },
+  //       {
+  //         index: '5-4',
+  //         label: 'AI API Log',
+  //         path: '/ai-api-log'
+  //       },
+  //       {
+  //         index: '5-5',
+  //         label: 'Operation Log',
+  //         path: '/operation-log'
+  //       },
+  //       {
+  //         index: '5-6',
+  //         label: 'Prompt Configuration',
+  //         path: '/prompt-configuration'
+  //       }
+  //     ]
+  //   }
+  // ]
 }
 getMenuList()
 //监听窗口大小
@@ -203,7 +203,6 @@ const changeRouter = (path: any) => {
 const handleCollapseClick = () => {
   isCollapse.value = !isCollapse.value
 }
-const menuRef = ref()
 
 // 友情链接
 const activeName = ref('1')

+ 10 - 2
src/views/Login/src/loginView.vue

@@ -392,7 +392,11 @@ const firstLoginTipsRef = ref()
             <span class="font_family icon-icon_username_b"></span>
           </template>
           <template #suffix>
-            <span v-if="isUserNameExit" class="font_family icon-icon_confirm_b confirm-icon"></span>
+            <span
+              v-if="isUserNameExit"
+              class="font_family icon-icon_confirm_b confirm-icon"
+              style="padding-top: 1px"
+            ></span>
           </template>
         </el-input>
         <div class="error" v-if="loginError.username">This account does not exist.</div>
@@ -450,7 +454,11 @@ const firstLoginTipsRef = ref()
             <span class="font_family icon-icon_username_b"></span>
           </template>
           <template #suffix>
-            <span v-if="isUserNameExit" class="font_family icon-icon_confirm_b confirm-icon"></span>
+            <span
+              v-if="isUserNameExit"
+              class="font_family icon-icon_confirm_b confirm-icon"
+              style="padding-top: 1px"
+            ></span>
           </template>
         </el-input>
         <div class="error" v-if="loginError.username">This account does not exist</div>

+ 183 - 81
src/views/PromptConfiguration/src/PromptConfiguration.vue

@@ -39,7 +39,15 @@ const addstepdata = () => {
 // 计算属性
 const testquestiontEmpty = computed(() => {
   // 去除首尾空格后检查长度
-  if(rolename.value.trim().length == 0 || professionalfield.value.trim().length == 0 || maintasks.value.trim().length == 0 || tablename.value.trim().length == 0 || tableDataList.value.length == 0 || stepData.value.length == 0 || formatList.value.length == 0) {
+  if (
+    rolename.value.trim().length == 0 ||
+    professionalfield.value.trim().length == 0 ||
+    maintasks.value.trim().length == 0 ||
+    tablename.value.trim().length == 0 ||
+    tableDataList.value.length == 0 ||
+    stepData.value.length == 0 ||
+    formatList.value.length == 0
+  ) {
     return true
   } else {
     return false
@@ -69,38 +77,50 @@ const getPromptConfiguration = () => {
 const missingmessage = ref()
 const SavePromptConfiguration = () => {
   missingmessage.value = ''
-  if(rolename.value != '' && professionalfield.value != '' && maintasks.value != '' && professionalfield.value != '' && tableDataList.value.length != 0 && stepData.value.length != 0 &&  formatList.value.length != 0 &&  outputvalue.value != '' && prompttextvalue.value != '') {
-    $api.SavePromptConfiguration({
-      role_name: rolename.value,
-      professional_field: professionalfield.value,
-      main_tasks: maintasks.value,
-      table_name: tablename.value,
-      table_description: tabledescription.value,
-      tableDataList: tableDataList.value,
-      stepData: stepData.value,
-      formatList: formatList.value,
-      outputvalue: outputvalue.value,
-      prompttextvalue: prompttextvalue.value,
-      id: editid.value
-    }).then((res) => {
-      if (res.code === 200) {
-        SaveedVisible.value = true
-        setTimeout(() => {
-          window.location.reload()
-        }, 3000)
-      }
-    })
+  if (
+    rolename.value != '' &&
+    professionalfield.value != '' &&
+    maintasks.value != '' &&
+    professionalfield.value != '' &&
+    tableDataList.value.length != 0 &&
+    stepData.value.length != 0 &&
+    formatList.value.length != 0 &&
+    outputvalue.value != '' &&
+    prompttextvalue.value != ''
+  ) {
+    $api
+      .SavePromptConfiguration({
+        role_name: rolename.value,
+        professional_field: professionalfield.value,
+        main_tasks: maintasks.value,
+        table_name: tablename.value,
+        table_description: tabledescription.value,
+        tableDataList: tableDataList.value,
+        stepData: stepData.value,
+        formatList: formatList.value,
+        outputvalue: outputvalue.value,
+        prompttextvalue: prompttextvalue.value,
+        id: editid.value
+      })
+      .then((res) => {
+        if (res.code === 200) {
+          SaveedVisible.value = true
+          setTimeout(() => {
+            window.location.reload()
+          }, 3000)
+        }
+      })
   } else {
-    if(rolename.value == '' || professionalfield.value == '' || maintasks.value == '') {
+    if (rolename.value == '' || professionalfield.value == '' || maintasks.value == '') {
       missingmessage.value += '系统角色配置, '
     }
-    if(tablename.value == '' || tableDataList.value.length == 0) {
+    if (tablename.value == '' || tableDataList.value.length == 0) {
       missingmessage.value += '表结构配置, '
     }
-    if(stepData.value.length == 0) {
+    if (stepData.value.length == 0) {
       missingmessage.value += '响应规则配置, '
     }
-    if(formatList.value.length == 0 || outputvalue.value == '') {
+    if (formatList.value.length == 0 || outputvalue.value == '') {
       missingmessage.value += '输出格式配置, '
     }
     missingmessage.value = missingmessage.value.substring(0, missingmessage.value.length - 2)
@@ -109,7 +129,8 @@ const SavePromptConfiguration = () => {
 }
 
 const EditPrompt = () => {
-  $api.EditPrompt({
+  $api
+    .EditPrompt({
       role_name: rolename.value,
       professional_field: professionalfield.value,
       main_tasks: maintasks.value,
@@ -121,7 +142,8 @@ const EditPrompt = () => {
       outputvalue: outputvalue.value,
       prompttextvalue: prompttextvalue.value,
       id: editid.value
-    }).then((res) => {
+    })
+    .then((res) => {
       if (res.code === 200) {
         prompttextvalue.value = res.data.complete_prompt
         promptValue.value = res.data.prompt_summary
@@ -133,16 +155,16 @@ const EditPrompt = () => {
 const exporttxt = () => {
   // 创建 Blob 对象
   const blob = new Blob([prompttext.value], { type: 'text/plain' })
-   // 创建下载链接
+  // 创建下载链接
   const link = document.createElement('a')
   link.href = URL.createObjectURL(blob)
-  const currentDate = moment();
-  const formattedDate = currentDate.format('YYYY-MM-DD HH:mm');
+  const currentDate = moment()
+  const formattedDate = currentDate.format('YYYY-MM-DD HH:mm')
   link.download = `${formattedDate} Prompt.txt` // 自定义文件名
-  
+
   // 触发下载
   link.click()
-  
+
   // 清理内存
   URL.revokeObjectURL(link.href)
 }
@@ -167,8 +189,17 @@ onMounted(() => {
   <div class="Title">
     <div>Prompt Configuration</div>
     <div>
-      <el-button class="el-button--default title-button" style="margin-right: 8px;" @click="PromptdialogVisible = true"><span class="font_family icon-icon_dashboard_b icon_dark" style="margin-right: 5px;"></span>变更日志查看</el-button>
-      <el-button class="el-button--main title-button" @click="SavePromptConfiguration"><span class="font_family icon-icon_save_b icon_dark" style="margin-right: 5px;"></span>保存生效</el-button>
+      <el-button
+        class="el-button--default title-button"
+        style="margin-right: 8px"
+        @click="PromptdialogVisible = true"
+        ><span class="font_family icon-icon_dashboard_b icon_dark" style="margin-right: 5px"></span
+        >变更日志查看</el-button
+      >
+      <el-button class="el-button--main title-button" @click="SavePromptConfiguration"
+        ><span class="font_family icon-icon_save_b icon_dark" style="margin-right: 5px"></span
+        >保存生效</el-button
+      >
     </div>
   </div>
   <el-dialog
@@ -179,7 +210,7 @@ onMounted(() => {
     align-center
     class="prompt-dialog"
     :close-on-click-modal="false"
-  > 
+  >
     <div class="diaolog-body">
       <div class="diaolog-content" v-for="(item, index) in ChangeLogList" :key="index">
         <div class="diaolog-flex">
@@ -188,7 +219,10 @@ onMounted(() => {
             <div class="dialog-person">{{ item.person }}</div>
           </div>
           <div>
-            <el-button class="el-button--default" @click="ViewPrompt(item.text)"><span class="font_family icon-icon_view_b icon_dark" style="margin-right: 5px;" ></span>查看完整Prompt</el-button>
+            <el-button class="el-button--default" @click="ViewPrompt(item.text)"
+              ><span class="font_family icon-icon_view_b icon_dark" style="margin-right: 5px"></span
+              >查看完整Prompt</el-button
+            >
           </div>
         </div>
         <div class="diaolog-text">
@@ -204,14 +238,19 @@ onMounted(() => {
     align-center
     class="prompt-dialog prompt-dialog-inner"
     :close-on-click-modal="false"
-  > 
+  >
     <template #header>
       <div class="dialog-header">
-        <div style="width: 55%;display: flex;justify-content: space-between;">
-          <div class="Back" @click="Backprompt"><span class="font_family icon-icon_previous_b icon_dark"></span>Back</div>
-          <div style="display: flex;align-items: center;">查看完整Prmopt</div>
+        <div style="width: 55%; display: flex; justify-content: space-between">
+          <div class="Back" @click="Backprompt">
+            <span class="font_family icon-icon_previous_b icon_dark"></span>Back
+          </div>
+          <div style="display: flex; align-items: center">查看完整Prmopt</div>
         </div>
-        <el-button class="el-button--default" @click="exporttxt"><span class="font_family icon-icon_import_b icon_dark" style="margin-right: 5px;" ></span>导出完整日志</el-button>
+        <el-button class="el-button--default" @click="exporttxt"
+          ><span class="font_family icon-icon_import_b icon_dark" style="margin-right: 5px"></span
+          >导出完整日志</el-button
+        >
       </div>
     </template>
     <pre class="diaolog-content">
@@ -223,62 +262,115 @@ onMounted(() => {
     <div class="prompt-border">
       <div class="prompt-title">系统角色设置</div>
       <div class="flex">
-        <div style="width: 50%; margin-right: 8px;">
+        <div style="width: 50%; margin-right: 8px">
           <div class="little-title">角色名称</div>
-          <el-input v-model="rolename" placeholder="为AI Robot创建一个角色名称" class="input-name" @change="EditPrompt"></el-input>
+          <el-input
+            v-model="rolename"
+            placeholder="为AI Robot创建一个角色名称"
+            class="input-name"
+            @change="EditPrompt"
+          ></el-input>
         </div>
-        <div style="width: 50%;">
+        <div style="width: 50%">
           <div class="little-title">专业领域</div>
-          <el-input v-model="professionalfield" placeholder="为AI Robot设定专业领域" class="input-name"></el-input>
+          <el-input
+            v-model="professionalfield"
+            placeholder="为AI Robot设定专业领域"
+            class="input-name"
+          ></el-input>
         </div>
       </div>
-      <div style="margin-top: 16px;">
+      <div style="margin-top: 16px">
         <div class="little-title">主要任务</div>
-        <el-input v-model="maintasks" type="textarea" placeholder="简要描述AI Robot的主要任务" class="input-name-textarea" :rows="3"></el-input>
+        <el-input
+          v-model="maintasks"
+          type="textarea"
+          placeholder="简要描述AI Robot的主要任务"
+          class="input-name-textarea"
+          :rows="3"
+        ></el-input>
       </div>
     </div>
     <!-- 表结构配置 -->
     <div class="prompt-border">
       <div class="prompt-title">表结构配置</div>
       <div class="flex">
-        <div style="width: 50%; margin-right: 8px;">
+        <div style="width: 50%; margin-right: 8px">
           <div class="little-title"><span class="stars_red">*</span>表名</div>
-          <el-input v-model="tablename" placeholder="创建一个表名" class="input-name" @change="EditPrompt"></el-input>
+          <el-input
+            v-model="tablename"
+            placeholder="创建一个表名"
+            class="input-name"
+            @change="EditPrompt"
+          ></el-input>
         </div>
-        <div style="width: 50%;">
+        <div style="width: 50%">
           <div class="little-title">表描述</div>
-          <el-input v-model="tabledescription" placeholder="简要描述表的用途" class="input-name"></el-input>
+          <el-input
+            v-model="tabledescription"
+            placeholder="简要描述表的用途"
+            class="input-name"
+          ></el-input>
         </div>
       </div>
-      <div style="margin-top: 16px;">
-        <el-button class="el-button--noborder--configuration prompt-button" @click="AddTableConfiguration">+ 添加字段</el-button>
-        <TableConfiguration ref="TableConfigurationref" :tableDataList="tableDataList"></TableConfiguration>
+      <div style="margin-top: 16px">
+        <el-button
+          class="el-button--noborder--configuration prompt-button"
+          @click="AddTableConfiguration"
+          >+ 添加字段</el-button
+        >
+        <TableConfiguration
+          ref="TableConfigurationref"
+          :tableDataList="tableDataList"
+        ></TableConfiguration>
       </div>
     </div>
     <!-- 响应规则配置 -->
     <div class="prompt-border">
       <div class="prompt-title">响应规则配置</div>
-      <div style="margin-top: 16px;">
-        <el-button class="el-button--noborder--configuration prompt-button" @click="addstepdata" v-if="stepData.length != 0">+ 添加步骤</el-button>
-        <RespnseConfiguration ref="RespnseConfigurationref" :stepDataprops="stepData"></RespnseConfiguration>
+      <div style="margin-top: 16px">
+        <el-button
+          class="el-button--noborder--configuration prompt-button"
+          @click="addstepdata"
+          v-if="stepData.length != 0"
+          >+ 添加步骤</el-button
+        >
+        <RespnseConfiguration
+          ref="RespnseConfigurationref"
+          :stepDataprops="stepData"
+        ></RespnseConfiguration>
       </div>
     </div>
     <!-- 输出格式配置 -->
     <div class="prompt-border">
       <div class="prompt-title">输出格式配置</div>
-      <div style="margin-top: 16px;">
-        <OutputConfiguration ref="OutputConfigurationref" :formatList="formatList" :outputvalue="outputvalue"></OutputConfiguration>
+      <div style="margin-top: 16px">
+        <OutputConfiguration
+          ref="OutputConfigurationref"
+          :formatList="formatList"
+          :outputvalue="outputvalue"
+        ></OutputConfiguration>
       </div>
     </div>
     <!-- 预览与测试 -->
-     <div class="propmt-border-colorful">
-        <div class="prompt-title" style="padding: 0 14px;">预览与测试</div>
-        <div style="margin-top: 16px;">
-          <PreviewTesting ref="PreviewTestingref" :prompttext="prompttextvalue" :testquestion="testquestiontEmpty" :promptValue="promptValue"></PreviewTesting>
-        </div>
-     </div>
-     <div class="propmt-save"><el-button class="el-button--main" @click="SavePromptConfiguration"><span class="font_family icon-icon_save_b icon_dark" style="margin-right: 5px;"></span>保存生效</el-button></div>
-     <!-- 保存失败 -->
+    <div class="propmt-border-colorful">
+      <div class="prompt-title" style="padding: 0 14px">预览与测试</div>
+      <div style="margin-top: 16px">
+        <PreviewTesting
+          ref="PreviewTestingref"
+          :prompttext="prompttextvalue"
+          :testquestion="testquestiontEmpty"
+          :promptValue="promptValue"
+        ></PreviewTesting>
+      </div>
+    </div>
+    <div class="propmt-save">
+      <el-button class="el-button--main" @click="SavePromptConfiguration"
+        ><span class="font_family icon-icon_save_b icon_dark" style="margin-right: 5px"></span
+        >保存生效</el-button
+      >
+    </div>
+    <!-- 保存失败 -->
     <el-dialog v-model="UnableSaveVisible" width="480">
       <div>{{ missingmessage }} missing.</div>
       <div>Please complete all required fields.</div>
@@ -294,19 +386,15 @@ onMounted(() => {
         </div>
       </template>
       <template #header>
-        <div class="cancel_header">
-          <span class="iconfont_icon iconfont_warning">
-            <svg class="iconfont icon_danger" aria-hidden="true">
-              <use xlink:href="#icon-icon_fail_fill_b"></use>
-            </svg>
-          </span>
+        <div class="unable-save-dialog-header">
+          <span class="font_family icon-icon_fail_fill_b"></span>
           Unable to Save
         </div>
       </template>
     </el-dialog>
     <!-- 保存成功 -->
     <el-dialog v-model="SaveedVisible" width="320" style="height: 212px">
-      <div style="text-align: center"><el-image :src="submitsucessful" style="width: 64px;" /></div>
+      <div style="text-align: center"><el-image :src="submitsucessful" style="width: 64px" /></div>
       <div style="text-align: center; margin-top: 20px">Saved successfully</div>
     </el-dialog>
   </div>
@@ -339,7 +427,9 @@ onMounted(() => {
   border-radius: 12px;
   background-clip: padding-box, border-box;
   background-origin: padding-box, border-box;
-  background-image: linear-gradient(to bottom, var(--color-mode), var(--color-mode)), linear-gradient(to bottom, #FF7500, #8112FF);
+  background-image:
+    linear-gradient(to bottom, var(--color-mode), var(--color-mode)),
+    linear-gradient(to bottom, #ff7500, #8112ff);
   margin-bottom: 8px;
   padding: 13px 0;
 }
@@ -405,7 +495,7 @@ onMounted(() => {
   padding: 16px;
   margin-bottom: 8px;
   border-radius: 6px;
-  line-height: 21px; 
+  line-height: 21px;
   white-space: break-spaces;
 }
 .diaolog-flex {
@@ -457,9 +547,9 @@ onMounted(() => {
   border-color: var(--color-btn-default-bg-hover);
   background-color: var(--color-btn-default-bg-hover);
   color: var(--color-theme);
-    span {
-      color: var(--color-theme);
-    }
+  span {
+    color: var(--color-theme);
+  }
 }
 .cancel_header {
   font-size: 18px;
@@ -475,4 +565,16 @@ onMounted(() => {
   display: flex;
   align-items: center;
 }
-</style>
+.unable-save-dialog-header {
+  display: flex;
+  align-items: center;
+  .font_family {
+    width: 16px;
+    height: 16px;
+    border-radius: 24px;
+    margin-right: 4px;
+    font-size: 14px;
+    color: #b53039;
+  }
+}
+</style>

+ 96 - 48
src/views/PromptConfiguration/src/components/OutputConfiguration.vue

@@ -7,7 +7,7 @@ const isEdit = ref(false)
 const UnableSaveVisible = ref(false)
 const outputoptions = ref([
   {
-    label:'JSON',
+    label: 'JSON',
     value: 'JSON'
   }
 ])
@@ -35,7 +35,7 @@ interface TypeItem {
   requirements: string
 }
 interface Props {
-  formatList: TypeItem []
+  formatList: TypeItem[]
   outputvalue: String
 }
 const props = withDefaults(defineProps<Props>(), {})
@@ -46,7 +46,7 @@ watch(
   () => props.formatList,
   (current) => {
     formatList.value = current
-    if(formatList.value == null) {
+    if (formatList.value == null) {
       formatList.value = []
     }
   }
@@ -90,16 +90,27 @@ const handleclickcancel = () => {
 
 // 保存字段
 const handleclicksave = () => {
-  if(isEdit.value) {
-    const target = formatList.value.find(obj => obj.name === temporaryname.value);
-    if(EditName.value == '' || EditDataType.value == '' || Editdescribe.value == '' || isSelectedvalue.value == '') {
+  if (isEdit.value) {
+    const target = formatList.value.find((obj) => obj.name === temporaryname.value)
+    if (
+      EditName.value == '' ||
+      EditDataType.value == '' ||
+      Editdescribe.value == '' ||
+      isSelectedvalue.value == ''
+    ) {
       UnableSaveVisible.value = true
     } else {
-      if(isSelectedvalue.value == '有条件必填' && Condition.value == '') {
+      if (isSelectedvalue.value == '有条件必填' && Condition.value == '') {
         UnableSaveVisible.value = true
       } else {
-        const newData = { name: EditName.value, type: EditDataType.value, describe: Editdescribe.value, selecttype: isSelectedvalue.value,requirements: Condition.value }
-        if (target) Object.assign(target, newData);
+        const newData = {
+          name: EditName.value,
+          type: EditDataType.value,
+          describe: Editdescribe.value,
+          selecttype: isSelectedvalue.value,
+          requirements: Condition.value
+        }
+        if (target) Object.assign(target, newData)
         EditName.value = ''
         EditDataType.value = ''
         Editdescribe.value = ''
@@ -110,16 +121,27 @@ const handleclicksave = () => {
       }
     }
   } else {
-      if(formatList.value.some(obj => Object.values(obj).includes(EditName.value))) {
-        const target = formatList.value.find(obj => obj.name === EditName.value);
-        if(EditName.value == '' || EditDataType.value == '' || Editdescribe.value == '' || isSelectedvalue.value == '') {
+    if (formatList.value.some((obj) => Object.values(obj).includes(EditName.value))) {
+      const target = formatList.value.find((obj) => obj.name === EditName.value)
+      if (
+        EditName.value == '' ||
+        EditDataType.value == '' ||
+        Editdescribe.value == '' ||
+        isSelectedvalue.value == ''
+      ) {
         UnableSaveVisible.value = true
       } else {
-        if(isSelectedvalue.value == '有条件必填' && Condition.value == '') {
+        if (isSelectedvalue.value == '有条件必填' && Condition.value == '') {
           UnableSaveVisible.value = true
         } else {
-          const newData = { name: EditName.value, type: EditDataType.value, describe: Editdescribe.value, selecttype: isSelectedvalue.value,requirements: Condition.value }
-          if (target) Object.assign(target, newData);
+          const newData = {
+            name: EditName.value,
+            type: EditDataType.value,
+            describe: Editdescribe.value,
+            selecttype: isSelectedvalue.value,
+            requirements: Condition.value
+          }
+          if (target) Object.assign(target, newData)
           EditName.value = ''
           EditDataType.value = ''
           Editdescribe.value = ''
@@ -130,10 +152,15 @@ const handleclicksave = () => {
         }
       }
     } else {
-      if(EditName.value == '' || EditDataType.value == '' || Editdescribe.value == '' || isSelectedvalue.value == '') {
+      if (
+        EditName.value == '' ||
+        EditDataType.value == '' ||
+        Editdescribe.value == '' ||
+        isSelectedvalue.value == ''
+      ) {
         UnableSaveVisible.value = true
       } else {
-        if(isSelectedvalue.value == '有条件必填' && Condition.value == '') {
+        if (isSelectedvalue.value == '有条件必填' && Condition.value == '') {
           UnableSaveVisible.value = true
         } else {
           formatList.value.push({
@@ -163,13 +190,11 @@ const handelclickdelete = (val: any) => {
 </script>
 <template>
   <div>
-    <div class="output-title">
-      <span class="stars_red">*</span>输出格式类型
-    </div>
+    <div class="output-title"><span class="stars_red">*</span>输出格式类型</div>
     <el-select
       v-model="outputvalue"
       placeholder="Select"
-      style="width: 480px;margin: 4px 0 16px 0;"
+      style="width: 480px; margin: 4px 0 16px 0"
     >
       <el-option
         v-for="item in outputoptions"
@@ -179,9 +204,11 @@ const handelclickdelete = (val: any) => {
       />
     </el-select>
     <div>
-      <el-button class="el-button--noborder--configuration prompt-button" @click="handleaddclick">+ 添加字段</el-button>
+      <el-button class="el-button--noborder--configuration prompt-button" @click="handleaddclick"
+        >+ 添加字段</el-button
+      >
     </div>
-    <div class="empty" v-if="formatList.length == 0 ">
+    <div class="empty" v-if="formatList.length == 0">
       <div>
         <div><img :src="addimg" width="100" /></div>
         <el-button @click="handleaddclick" class="el-button--main">+ 添加字段</el-button>
@@ -194,16 +221,29 @@ const handelclickdelete = (val: any) => {
           <div class="output-item-type">{{ item.type }}</div>
         </div>
         <div class="output-flex-left">
-          <el-button class="el-button--blue" style="height: 24px;width: 24px;" @click="handelclickedit(item)">
+          <el-button
+            class="el-button--blue"
+            style="height: 24px; width: 24px"
+            @click="handelclickedit(item)"
+          >
             <span class="font_family icon-icon_edit_b icon_dark"></span>
           </el-button>
-          <el-button @click="handelclickdelete(item)" class="el-button--blue" style="height: 24px;width: 24px;">
+          <el-button
+            @click="handelclickdelete(item)"
+            class="el-button--blue"
+            style="height: 24px; width: 24px"
+          >
             <span class="font_family icon-icon_delete_b icon_dark"></span>
           </el-button>
         </div>
       </div>
       <div class="output-describe">{{ item.describe }}</div>
-      <div class="output-select">{{ item.selecttype }}<span class="output-select" v-if="item.selecttype === '有条件必填'">({{item.requirements}})</span></div>
+      <div class="output-select">
+        {{ item.selecttype
+        }}<span class="output-select" v-if="item.selecttype === '有条件必填'"
+          >({{ item.requirements }})</span
+        >
+      </div>
     </div>
     <el-dialog
       v-model="editdialogVisible"
@@ -213,19 +253,15 @@ const handelclickdelete = (val: any) => {
       :close-on-click-modal="false"
       class="dialog-edit"
       width="480"
-    > 
+    >
       <div class="dialog-title"><span class="stars_red">*</span>字段名称</div>
-      <el-input v-model="EditName" style="margin-bottom: 16px;"></el-input>
+      <el-input v-model="EditName" style="margin-bottom: 16px"></el-input>
       <div class="dialog-title"><span class="stars_red">*</span>数据类型</div>
-      <el-input v-model="EditDataType" style="margin-bottom: 16px;"></el-input>
+      <el-input v-model="EditDataType" style="margin-bottom: 16px"></el-input>
       <div class="dialog-title"><span class="stars_red">*</span>描述</div>
-      <el-input type="textarea" v-model="Editdescribe" style="margin-bottom: 16px;"></el-input>
+      <el-input type="textarea" v-model="Editdescribe" style="margin-bottom: 16px"></el-input>
       <div class="dialog-title"><span class="stars_red">*</span>是否必填</div>
-      <el-select
-        v-model="isSelectedvalue"
-        placeholder="Select"
-        style="margin-bottom: 16px;"
-      >
+      <el-select v-model="isSelectedvalue" placeholder="Select" style="margin-bottom: 16px">
         <el-option
           v-for="item in isSelected"
           :key="item.value"
@@ -233,15 +269,19 @@ const handelclickdelete = (val: any) => {
           :value="item.value"
         />
       </el-select>
-      <div v-if="isSelectedvalue === '有条件必填'" class="dialog-title"><span class="stars_red">*</span>条件要求</div>
-      <el-input v-if="isSelectedvalue === '有条件必填'"  v-model="Condition"></el-input>
+      <div v-if="isSelectedvalue === '有条件必填'" class="dialog-title">
+        <span class="stars_red">*</span>条件要求
+      </div>
+      <el-input v-if="isSelectedvalue === '有条件必填'" v-model="Condition"></el-input>
       <template #footer>
         <div class="dialog-footer">
           <el-button class="dialog_button" type="default" @click="handleclickcancel">
-            <span class="font_family icon-icon_return_b icon_dark" style="margin-right: 5px;"></span>Cancel
+            <span class="font_family icon-icon_return_b icon_dark" style="margin-right: 5px"></span
+            >Cancel
           </el-button>
           <el-button class="el-button--dark dialog_button" @click="handleclicksave">
-            <span class="font_family icon-icon_save_b icon_dark" style="margin-right: 5px;"></span>Save
+            <span class="font_family icon-icon_save_b icon_dark" style="margin-right: 5px"></span
+            >Save
           </el-button>
         </div>
       </template>
@@ -260,12 +300,8 @@ const handelclickdelete = (val: any) => {
         </div>
       </template>
       <template #header>
-        <div class="cancel_header">
-          <span class="iconfont_icon iconfont_warning">
-            <svg class="iconfont icon_danger" aria-hidden="true">
-              <use xlink:href="#icon-icon_fail_fill_b"></use>
-            </svg>
-          </span>
+        <div class="dialog-header">
+          <span class="font_family icon-icon_fail_fill_b"></span>
           Unable to Save
         </div>
       </template>
@@ -323,7 +359,7 @@ const handelclickdelete = (val: any) => {
   margin: 6px 0;
 }
 .output-select {
-  color:var(--color-output-select-text);
+  color: var(--color-output-select-text);
   font-size: 12px;
 }
 :deep(.dialog_button) {
@@ -331,11 +367,23 @@ const handelclickdelete = (val: any) => {
   height: 40px;
 }
 :deep(.el-dialog) {
-  box-shadow: 2px 2px 12px 0px rgba(0, 0, 0, 0.25) ;
+  box-shadow: 2px 2px 12px 0px rgba(0, 0, 0, 0.25);
 }
 .dialog-title {
   color: var(--color-neutral-2);
   font-size: 12px;
   margin-bottom: 8px;
 }
-</style>
+.dialog-header {
+  display: flex;
+  align-items: center;
+  .font_family {
+    width: 16px;
+    height: 16px;
+    border-radius: 24px;
+    margin-right: 4px;
+    font-size: 14px;
+    color: #b53039;
+  }
+}
+</style>

+ 39 - 21
src/views/Report/src/components/ReportDetail/src/ReportDetail.vue

@@ -4,6 +4,7 @@ import FieldsTable from './components/FieldsTable.vue'
 import { useCalculatingHeight } from '@/hooks/calculatingHeight'
 import { useUserStore } from '@/stores/modules/user'
 import { useRoute } from 'vue-router'
+import dayjs from 'dayjs'
 
 const route = useRoute()
 
@@ -18,14 +19,41 @@ const ManageReportFieldsRef = ref()
 const fieldsTableRef = ref()
 
 const filterList = ref([])
-const handleFilterData = (data) => {
+const reportName = ref('')
+const getFilterListData = () => {
+  $api
+    .getReportDetailFilterConfig({
+      serial_no: route.query.id as string
+    })
+    .then((res) => {
+      handleFilterData(res.data.filtersList, res.data.reportName)
+      nextTick(() => {
+        fieldsTableRef.value.getTableData()
+      })
+    })
+}
+onMounted(() => {
+  getFilterListData()
+})
+
+const handleFilterData = (filterData, name) => {
+  reportName.value = name
   if (filterList.value.length) return
-  filterList.value = data.map((item) => {
+  filterList.value = filterData.map((item) => {
     let curData: any = {}
     if (item.data_type === 'string') {
       curData.value = ''
     } else if (item.data_type === 'number' || item.data_type === 'date') {
       curData.value = []
+    } else if (item.data_type === 'select') {
+      curData.options = item.options
+      curData.value = ''
+    }
+    if (item.label === 'ETD') {
+      curData.value = [
+        dayjs().subtract(2, 'month').startOf('month').format('MM/DD/YYYY'),
+        dayjs().add(1, 'month').format('MM/DD/YYYY')
+      ]
     }
     return {
       label: item.label,
@@ -41,7 +69,7 @@ const handleClickManageFields = () => {
 }
 
 const handelSearchFilters = () => {
-  fieldsTableRef.value.handleSearch()
+  fieldsTableRef.value.getTableData()
 }
 const handleClickReset = () => {
   filterList.value.forEach((item) => {
@@ -65,7 +93,7 @@ const applyNewColumn = () => {
 <template>
   <div>
     <div class="Title">
-      <span>Shipment Status Report</span>
+      <span>{{ reportName }}</span>
       <div>
         <el-popover placement="bottom" width="195" :visible="DownloadVisible">
           <p class="download-item" @click="handleClickDownload('xlsx')">Excel(.xlsx)</p>
@@ -121,10 +149,12 @@ const applyNewColumn = () => {
             v-if="item.type === 'string'"
             placeholder="Please enter..."
             v-model="item.value"
+            clearable
           ></el-input>
           <el-select
             v-else-if="item.type === 'select'"
             v-model="item.value"
+            clearable
             placeholder="Please select..."
           >
             <el-option
@@ -140,6 +170,7 @@ const applyNewColumn = () => {
               class="no-spinner"
               type="number"
               v-model="item.value[0]"
+              clearable
             ></el-input>
             -
             <el-input
@@ -147,6 +178,7 @@ const applyNewColumn = () => {
               class="no-spinner"
               type="number"
               v-model="item.value[1]"
+              clearable
             ></el-input>
           </div>
           <div v-if="item.type === 'date'" style="display: flex; gap: 4px; align-items: center">
@@ -157,25 +189,10 @@ const applyNewColumn = () => {
               start-placeholder="Start date"
               end-placeholder="End date"
               style="height: 32px"
+              clearable
               value-format="MM/DD/YYYY"
               :format="formatDate"
             />
-
-            <!-- <el-date-picker
-              v-model="item.value[0]"
-              type="date"
-              placeholder="Pick a Date"
-              :format="formatDate"
-              valueFormat="MM/DD/YYYY"
-            />
-            -
-            <el-date-picker
-              v-model="item.value[1]"
-              type="date"
-              placeholder="Pick a Date"
-              :format="formatDate"
-              valueFormat="MM/DD/YYYY"
-            /> -->
           </div>
         </div>
       </div>
@@ -184,8 +201,9 @@ const applyNewColumn = () => {
       ref="fieldsTableRef"
       :containerHeight="containerHeight"
       :filterData="filterList"
-      @filterOptionsLoaded="handleFilterData"
     ></FieldsTable>
+
+    <!-- @filterOptionsLoaded="handleFilterData" -->
   </div>
 </template>
 

+ 41 - 25
src/views/Report/src/components/ReportDetail/src/components/FieldsTable.vue

@@ -2,7 +2,7 @@
 import { ref, onMounted } from 'vue'
 import { type VxeGridInstance, type VxeGridProps } from 'vxe-table'
 import { useRowClickStyle } from '@/hooks/rowClickStyle'
-import { formatNumber } from '@/utils/tools'
+import { formatNumber, formatTimezone } from '@/utils/tools'
 import dayjs from 'dayjs'
 import { autoWidth } from '@/utils/table'
 import { useRoute } from 'vue-router'
@@ -85,18 +85,29 @@ const handleColumns = (columns: any) => {
         slots: { default: 'status' }
       }
     }
+    // 格式化
+    if (item.formatter === 'date' || item.formatter === 'dateTime') {
+      curColumn = {
+        ...curColumn,
+        formatter: ({ cellValue }: any) => formatTimezone(cellValue, '', '', 'MM/DD/YYYY HH:mm')
+      }
+    } else if (item.formatter === 'number') {
+      curColumn = {
+        ...curColumn,
+        formatter: ({ cellValue }: any) => formatNumber(Number(cellValue), item?.digits)
+      }
+    }
     return curColumn
   })
   return newColumns
 }
-const emit = defineEmits(['filterOptionsLoaded'])
 
 // 获取表格数据
 const getTableData = (isPageChange?: boolean) => {
   tableLoadingTable.value = true
   let queryParams = {}
   props.filterData.forEach((item) => {
-    if (item.type === 'string') {
+    if (item.type === 'string' || item.type === 'select') {
       queryParams[item.field] = item.value
     } else if (item.type === 'number' || item.type === 'date') {
       queryParams[item.field] = item.value
@@ -108,6 +119,8 @@ const getTableData = (isPageChange?: boolean) => {
       ps: pageInfo.value.pageSize,
       rc: isPageChange ? pageInfo.value.total : -1,
       serial_no: route.query.id,
+      sortByField: sortBy.value,
+      sortByOrder: sortOrder.value,
       ...queryParams
     })
     .then((res: any) => {
@@ -122,6 +135,7 @@ const getTableData = (isPageChange?: boolean) => {
         allTable.value.columns = handleColumns(res.data.tableColumns)
 
         tmpSearch = res.data.tmp_search
+        tmpMapping = res.data.tmp_mapping
 
         reportName.value = res.data.reportName
 
@@ -129,24 +143,18 @@ const getTableData = (isPageChange?: boolean) => {
         sortByOptions.value = sortByData.options
         sortBy.value = sortByData.field
         sortOrder.value = sortByData.order
-        emit('filterOptionsLoaded', res.data.filtersList)
       }
     })
     .finally(() => {
       nextTick(() => {
         tableRef.value && autoWidth(tableData.value, tableRef.value)
-
         tableLoadingTable.value = false
       })
     })
 }
 
-// 查询时调用接口
-const handleSearch = () => {
-  getTableData()
-}
-
 let tmpSearch = ''
+let tmpMapping = ''
 // 下载
 const getExportTableData = async (type: string, column: any) => {
   if (column) {
@@ -161,16 +169,22 @@ const getExportTableData = async (type: string, column: any) => {
     column = newColumns
   }
   exportLoading.value = true
-  await $api.getReportAllTableData({ tmp_search: tmpSearch }).then((res: any) => {
-    if (res.code === 200) {
-      allTable.value.data = res.data.Data || []
-      if (allTable.value.data.length > 20000) {
-        type = 'csv'
+  await $api
+    .getReportAllTableData({ tmp_search: tmpSearch, tmp_mapping: tmpMapping })
+    .then((res: any) => {
+      if (res.code === 200) {
+        allTable.value.data = res.data.Data || []
+        if (allTable.value.data.length > 20000) {
+          type = 'csv'
+        }
+      } else if (res.code === 500) {
+        ElMessage.error(res.data.msg)
       }
-    } else if (res.code === 500) {
-      ElMessage.error(res.data.msg)
-    }
-  })
+    })
+    .catch((err: any) => {
+      exportLoading.value = false
+      return
+    })
   await nextTick(async () => {
     await autoWidth(allTable.value, allTableRef.value)
   })
@@ -187,12 +201,8 @@ const getExportTableData = async (type: string, column: any) => {
 // 实现行点击样式
 useRowClickStyle(tableRef)
 
-onMounted(() => {
-  getTableData()
-})
-
 defineExpose({
-  handleSearch,
+  getTableData,
   getExportTableData
 })
 </script>
@@ -211,11 +221,17 @@ defineExpose({
           <el-select
             style="width: 200px; margin: 0 8px"
             v-model="sortBy"
+            @change="getTableData()"
             placeholder="Please select..."
           >
             <el-option v-for="item in sortByOptions" :key="item" :label="item" :value="item" />
           </el-select>
-          <el-select style="width: 124px" v-model="sortOrder" placeholder="Please select...">
+          <el-select
+            @change="getTableData()"
+            style="width: 124px"
+            v-model="sortOrder"
+            placeholder="Please select..."
+          >
             <el-option
               v-for="item in sortOptions"
               :key="item.value"

+ 3 - 1
src/views/Report/src/components/ReportSchedule/src/ReportSchedule.vue

@@ -41,6 +41,8 @@ onMounted(() => {
     .then((res: any) => {
       if (res.code === 200) {
         pageData.value = res.data.showData
+        // 初始化的时候如果Report Data Time Range 有值 则调用一次查询
+        changeTimeRange(pageData.value.timeRange)
       }
     })
     .then(() => {
@@ -107,7 +109,7 @@ const handleSave = () => {
 <template>
   <div v-vloading="loading">
     <div class="Title">
-      <span>Schedule Configuration - Shipment Status Report</span>
+      <span>Schedule Configuration - {{ pageData.reportName }}</span>
       <el-button @click="handleSave" class="el-button--main save_button"
         ><span class="font_family icon-icon_save_b icon_dark" style="margin-right: 5px"></span
         >Save</el-button

+ 1 - 0
src/views/Report/src/components/ReportSchedule/src/components/FieldsTable.vue

@@ -153,6 +153,7 @@ defineExpose({
           <el-select
             style="width: 160px; margin: 0 8px"
             v-model="orderBy"
+            @change="getTableData()"
             placeholder="Please select..."
           >
             <el-option

+ 104 - 7
src/views/TemplateManagement/src/TemplateManagement.vue

@@ -57,6 +57,67 @@ const handleCreate = () => {
     name: 'Create Report Template'
   })
 }
+
+import { debounce } from 'lodash'
+import axios from 'axios'
+
+const emit = defineEmits(['changeData'])
+const changeData = (val: string[]) => {
+  // 同步选中状态
+  emit('changeData', val)
+}
+
+interface ListItem {
+  label: string
+  id: string
+  code: string
+}
+
+const options = ref<ListItem[]>([])
+const loading = ref(false)
+const currentController = ref<AbortController | null>(null)
+
+const handleVisibleChange = (visible) => {
+  !visible && (options.value = [])
+}
+const remoteMethod = (query: string) => {
+  currentController.value?.abort()
+
+  const newController = new AbortController()
+  currentController.value = newController
+  loading.value = true
+
+  $api
+    .getSpecificRolesPartyID({ term: query }, { signal: newController.signal })
+    .then((res) => {
+      if (!newController.signal.aborted && res.code === 200) {
+        options.value = (res.data || []).map((item) => ({
+          id: item.id,
+          code: item.code,
+          label: item.label
+        }))
+      }
+    })
+    .catch((err) => {
+      options.value = []
+      if (!axios.isCancel(err) && !newController.signal.aborted) {
+        ElMessage.error('Failed to load options')
+      }
+    })
+    .finally(() => {
+      // 仅当这是最新请求时,才关闭 loading
+      if (currentController.value === newController) {
+        loading.value = false
+      }
+    })
+}
+
+// 防抖版本(可选)
+const debouncedRemoteMethod = debounce(remoteMethod, 200)
+
+onUnmounted(() => {
+  currentController.value?.abort()
+})
 </script>
 <template>
   <div class="dashboard">
@@ -108,14 +169,34 @@ const handleCreate = () => {
             />
           </el-select>
         </div>
-        <div class="tips_filter">
-          <el-select v-model="queryData.party_id" clearable placeholder="Party ID">
+        <div class="party-id-tips-filter">
+          <el-select
+            v-model="queryData.party_id"
+            multiple
+            filterable
+            reserve-keyword
+            placeholder="Party IDs"
+            :loading="loading"
+            collapse-tags
+            collapse-tags-tooltip
+            :max-collapse-tags="1"
+            popper-class="part-id-select-popper"
+            :filter-method="debouncedRemoteMethod"
+            @change="changeData"
+            @visible-change="handleVisibleChange"
+          >
             <el-option
-              v-for="item in aiModelList"
-              :key="item.value"
-              :label="item.label"
-              :value="item.value"
-            />
+              v-for="item in options"
+              :key="item.code"
+              :label="item.code"
+              :value="item.code"
+            >
+              <div class="select-option">
+                <el-checkbox :model-value="queryData.party_id.includes(item.code)">
+                  <span class="text-ellipsis" style="width: 240px">{{ item.code }}</span>
+                </el-checkbox>
+              </div>
+            </el-option>
           </el-select>
         </div>
 
@@ -156,6 +237,16 @@ const handleCreate = () => {
   max-width: 190px;
   margin-right: 8px;
 }
+.party-id-tips-filter {
+  flex: 1;
+  height: 30px;
+  width: 280px;
+  max-width: 280px;
+  margin-right: 8px;
+  :deep(.el-tag) {
+    max-width: 220px !important;
+  }
+}
 .input-tips_filter {
   flex: 1;
   max-width: 320px;
@@ -183,4 +274,10 @@ const handleCreate = () => {
   position: relative;
   background-color: var(--color-mode);
 }
+.text-ellipsis {
+  display: inline-block;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  overflow: hidden;
+}
 </style>

+ 49 - 448
src/views/TemplateManagement/src/components/CreateReportTemplate/src/CreateReportTemplate.vue

@@ -3,9 +3,7 @@ import { useRouter, useRoute } from 'vue-router'
 import partyIDSelect from './components/partyIDSelect.vue'
 import GroupNameSelect from './components/GroupNameSelect.vue'
 import AccountSelect from './components/AccountSelect.vue'
-import { VueDraggable } from 'vue-draggable-plus'
-import AdjustmentField from './components/AdjustmentField.vue'
-import { cloneDeep } from 'lodash'
+import ReportFieldsConfiguration from './components/ReportFieldsConfiguration.vue'
 
 const router = useRouter()
 const route = useRoute()
@@ -30,6 +28,7 @@ onMounted(() => {
           reportLevel: data.reportLevel,
           reportDescription: data.reportDescription
         }
+        oldReportLevel.value = data.reportLevel
         fieldsList.value = data.reportFields.map((item) => {
           return {
             ...item,
@@ -72,13 +71,15 @@ interface Field {
   value?: string
   isFilter: boolean
   isSort: boolean
+  mapping?: { system: string; converted: string }[]
   groupName: string
+  isFieldDataMapping?: string
 }
 const fieldsList = ref<Field[]>([])
 const levelOptions = [
   {
-    label: 'Shipment level',
-    value: 'Shipment level'
+    label: 'Shipment Level',
+    value: 'Shipment Level'
   },
   {
     label: 'Container Level',
@@ -95,151 +96,6 @@ const generate8DigitUnique = () => {
   return Math.floor(10000000 + Math.random() * 90000000).toString()
 }
 
-const changeFieldConfig = (state: any, field: string, index: number, key: string) => {
-  if (!state) return
-  fieldsList.value.forEach((item) => {
-    if (item.field === field) {
-      item[key] = false
-    }
-  })
-  const firstMatch = fieldsList.value[index]
-  if (firstMatch) {
-    firstMatch[key] = true
-  }
-}
-
-const handleDeleteField = (index: number, uniqueId: string) => {
-  fieldsList.value.splice(index, 1)
-
-  Object.keys(copyFieldsList.value).forEach((key) => {
-    copyFieldsList.value[key] = copyFieldsList.value[key].filter(
-      (item) => item.uniqueId !== uniqueId
-    )
-  })
-}
-const copyFieldsList = ref({})
-const handleCopyField = (index: number) => {
-  const curField = cloneDeep(fieldsList.value[index])
-  curField.displayName = `${curField.displayName} (Copy)`
-  curField.isFilter = false
-  curField.isSort = false
-  curField.uniqueId = generate8DigitUnique()
-  if (copyFieldsList.value[curField.field]) {
-    copyFieldsList.value[curField.field].push(cloneDeep(curField))
-  } else {
-    copyFieldsList.value[curField.field] = [cloneDeep(curField)]
-  }
-  fieldsList.value.splice(index + 1, 0, cloneDeep(curField))
-}
-
-const AdjustmentFieldRef = ref()
-// 打开定制表格弹窗
-const handleCustomizeColumns = () => {
-  if (!infoData.value.reportLevel) {
-    ElMessage.warning('Please select the report level.')
-    return
-  }
-  const params = {
-    serial_no: '',
-    level: infoData.value.reportLevel
-  }
-  const seen = new Set()
-  const uniqueArray = fieldsList.value.filter((item) => {
-    if (seen.has(item.field)) {
-      return false // 已存在,跳过
-    }
-    seen.add(item.field)
-    return true // 第一次出现,保留
-  })
-
-  AdjustmentFieldRef.value.openDialog(
-    params,
-    -220,
-    'Drag item over to this selection or click "add" icon to show the field on report template list',
-    uniqueArray
-  )
-}
-
-const newFieldInfo = ref<{
-  name: string
-  fieldType: 'Blank' | 'Fixed Value'
-  value: string
-}>({
-  name: '',
-  fieldType: 'Blank',
-  value: ''
-})
-const addNewFieldVisible = ref(false)
-// 添加新字段
-const handleAddNewField = () => {
-  addNewFieldVisible.value = true
-}
-const handleFieldTypeChange = () => {
-  newFieldInfo.value.value = ''
-}
-const addNewField = () => {
-  if (!newFieldInfo.value.name?.trim()) {
-    ElMessage.warning('Please enter the new field name.')
-    return
-  }
-
-  if (newFieldInfo.value.fieldType === 'Fixed Value' && !newFieldInfo.value.value?.trim()) {
-    ElMessage.warning('Please enter the fixed value.')
-    return
-  }
-  fieldsList.value.unshift({
-    uniqueId: generate8DigitUnique(),
-    field: newFieldInfo.value.name,
-    title: newFieldInfo.value.name,
-    displayName: newFieldInfo.value.name,
-    value: newFieldInfo.value.value,
-    fieldType: 'Custom',
-    groupName: '',
-    isFilter: false,
-    isSort: false
-  })
-  newFieldInfo.value = {
-    name: '',
-    fieldType: 'Blank',
-    value: ''
-  }
-  addNewFieldVisible.value = false
-}
-// 调整应用字段
-const handleApplay = (data: any) => {
-  const customizeData = fieldsList.value.filter((item: any) => item.field_type === 'Custom')
-  fieldsList.value = data.map((item: any) => {
-    return {
-      ...item,
-      label: item.label,
-      title: item.title || item.label,
-      displayName: item.displayName || item.label,
-      isFilter: !!item.isFilter,
-      isSort: !!item.isSort,
-      uniqueId: item.uniqueId || generate8DigitUnique()
-    }
-  })
-  fieldsList.value = [...customizeData, ...fieldsList.value]
-
-  const validFields = new Set(fieldsList.value.map((item) => item.field))
-  for (const field in copyFieldsList.value) {
-    if (!validFields.has(field)) {
-      delete copyFieldsList.value[field]
-    }
-  }
-
-  // === 第三步:从后往前插入副本(不修改原始项)===
-  for (let i = fieldsList.value.length - 1; i >= 0; i--) {
-    const field = fieldsList.value[i].field
-    const copies = copyFieldsList.value[field]
-
-    if (copies?.length) {
-      // 插入副本到原项后面
-      fieldsList.value.splice(i + 1, 0, ...copies)
-    }
-  }
-}
-
 const accessControlType = ref('All Users')
 const detailRef: Ref<HTMLElement | null> = ref(null)
 
@@ -248,6 +104,11 @@ const specificRoles = ref({
   groupName: [],
   systemAccount: []
 })
+
+const changeControlType = () => {
+  specificRoles.value = { partyId: [], groupName: [], systemAccount: [] }
+}
+
 const changePartyId = (val: string[]) => {
   specificRoles.value.partyId = val
 }
@@ -259,13 +120,22 @@ const changeAccount = (val: string[]) => {
   specificRoles.value.systemAccount = val
 }
 
-const fieldLoading = ref(false)
-const handleRightRemove = () => {}
-
 const handleCancel = () => {
   router.push('/template-management')
 }
 
+const fieldsConfigurationRef = ref()
+const oldReportLevel = ref('')
+const changeLevel = (val: string) => {
+  fieldsConfigurationRef.value.changeReportLevel(val, oldReportLevel.value)
+}
+const cancelChangeLevel = (val: string) => {
+  infoData.value.reportLevel = val
+}
+const changeOldLevel = (val: string) => {
+  oldReportLevel.value = val
+}
+
 const handlePageSave = () => {
   let verified = true
   if (!infoData.value.reportName.trim()) {
@@ -282,11 +152,13 @@ const handlePageSave = () => {
   }
   if (
     accessControlType.value === 'Specific Roles' &&
-    specificRoles.value.partyId?.length === 0 &&
-    specificRoles.value.groupName?.length === 0 &&
-    specificRoles.value.systemAccount?.length === 0
+    !specificRoles.value.partyId?.length &&
+    !specificRoles.value.groupName?.length &&
+    !specificRoles.value.systemAccount?.length
   ) {
-    ElMessage.warning('Please select Party ID or Group Name for Specific Roles access control.')
+    ElMessage.warning(
+      'Please select Party ID or Group Name or KLN ONLINE Account for Specific Roles access control.'
+    )
     verified = false
   }
   if (!verified) {
@@ -301,7 +173,7 @@ const handlePageSave = () => {
     party_ids: specificRoles.value.partyId || [],
     group_names: specificRoles.value.groupName || [],
     system_account: specificRoles.value.systemAccount || [],
-    fieldsList: fieldsList.value
+    fieldsList: fieldsConfigurationRef.value.getFieldsList()
   }
   let serial_no = ''
   if (route.query.copy !== 't' && route.query.serial_no) {
@@ -324,7 +196,7 @@ const handlePageSave = () => {
 </script>
 <template>
   <div class="dashboard" v-vloading="pageLoading">
-    <div class="Title">
+    <div class="title">
       <span>Create New Report Template</span>
       <div class="button-group">
         <el-button type="default" @click="handleCancel">
@@ -354,7 +226,11 @@ const handlePageSave = () => {
                 <span style="color: var(--color-danger)">*</span>
                 <span>Report Level</span>
               </div>
-              <el-select v-model="infoData.reportLevel" placeholder="Please enter...">
+              <el-select
+                v-model="infoData.reportLevel"
+                @change="changeLevel"
+                placeholder="Please enter..."
+              >
                 <el-option
                   v-for="item in levelOptions"
                   :label="item.label"
@@ -377,108 +253,21 @@ const handlePageSave = () => {
           </div>
         </div>
       </div>
-      <div class="fields-configuration template-box">
-        <div class="header">
-          <span>Report Fields Configuration</span>
-
-          <div class="right-option">
-            <el-button
-              class="el-button--dark"
-              @click="handleAddNewField"
-              style="width: 148px; padding-top: 11px"
-            >
-              <span style="margin-right: 3px" class="font_family icon-icon_add_b"></span>
-              <span>Add New Field</span>
-            </el-button>
-            <el-button
-              v-if="fieldsList.length > 0"
-              class="el-button--dark"
-              @click="handleCustomizeColumns()"
-              style="width: 110px; padding-top: 11px"
-            >
-              <span style="margin-right: 3px" class="font_family icon-icon_add_b"></span>
-              <span>Select Field</span>
-            </el-button>
-          </div>
-        </div>
-        <div class="content-box">
-          <div class="empty-box" v-if="fieldsList.length === 0">
-            <el-button class="el-button--dark" @click="handleCustomizeColumns">
-              <span class="font_family icon-icon_add_b"></span>Add/Edit Field
-            </el-button>
-            <p>No field selected. click “Add Field” to get started.</p>
-          </div>
-          <div class="fields-list" v-else>
-            <VueDraggable
-              v-vloading="fieldLoading"
-              v-model="fieldsList"
-              class="column-list"
-              ghost-class="ghost-column"
-              :forceFallback="true"
-              fallback-class="fallback-class"
-              group="customizeColumns"
-              item-key="uniqueId"
-              @end="handleRightRemove"
-              handle=".handle-draggable"
-            >
-              <div
-                class="field-item"
-                v-for="(fieldItem, index) in fieldsList"
-                :key="fieldItem.uniqueId"
-              >
-                <span
-                  class="font_family icon-icon_dragsort__b draggable-icon handle-draggable"
-                  style="margin-right: 12px; font-size: 16px"
-                ></span>
-                <div class="label handle-draggable">
-                  <span style="font-weight: 700">[{{ fieldItem.field }}]</span>
-                  <span style="margin-left: 8px">{{ fieldItem.title }}</span>
-                </div>
-                <el-input
-                  :id="fieldItem.uniqueId"
-                  :name="fieldItem.uniqueId"
-                  class="display-name"
-                  v-model="fieldItem.displayName"
-                  placeholder="Display Name in Report"
-                ></el-input>
-                <div class="actions">
-                  <div class="checkbox-group">
-                    <el-checkbox
-                      :disabled="
-                        fieldItem.fieldType !== 'System' ||
-                        fieldItem.groupName === 'Container Status' ||
-                        fieldItem.groupName === 'Milestone'
-                      "
-                      v-model="fieldItem.isFilter"
-                      @change="changeFieldConfig($event, fieldItem.field, index, 'isFilter')"
-                      >Filter</el-checkbox
-                    >
-                    <el-checkbox
-                      :disabled="fieldItem.fieldType !== 'System'"
-                      v-model="fieldItem.isSort"
-                      @change="changeFieldConfig($event, fieldItem.field, index, 'isSort')"
-                      >Sort</el-checkbox
-                    >
-                  </div>
-                  <span
-                    style="margin-right: 4px"
-                    @click="handleCopyField(index)"
-                    class="font_family icon-icon_clone_b"
-                  ></span>
-                  <span
-                    @click="handleDeleteField(index, fieldItem.uniqueId)"
-                    class="font_family icon-icon_delete_b"
-                  ></span>
-                </div>
-              </div>
-            </VueDraggable>
-          </div>
-        </div>
-      </div>
+      <ReportFieldsConfiguration
+        :report-level="infoData.reportLevel"
+        :pageData="fieldsList"
+        @confirm-change-level="changeOldLevel"
+        @cancel-change-level="cancelChangeLevel"
+        ref="fieldsConfigurationRef"
+      ></ReportFieldsConfiguration>
       <div class="report-access-control template-box">
         <div class="header">Report Access Control</div>
         <div class="content-box">
-          <el-radio-group class="radio-group" v-model="accessControlType">
+          <el-radio-group
+            class="radio-group"
+            @change="changeControlType"
+            v-model="accessControlType"
+          >
             <el-radio class="radio-item" value="All Users">
               <template #default>
                 <div class="radio-content">
@@ -540,58 +329,11 @@ const handlePageSave = () => {
         </div>
       </div>
     </div>
-
-    <AdjustmentField @apply="handleApplay" ref="AdjustmentFieldRef" />
-    <el-dialog
-      class="add-new-field-dialog"
-      title="Add New Field"
-      v-model="addNewFieldVisible"
-      width="480"
-    >
-      <div>
-        <div class="field-item">
-          <div class="label">
-            <span class="required-symbol">*</span>
-            <span>New Field Name</span>
-          </div>
-          <el-input placeholder="Please enter..." v-model="newFieldInfo.name"></el-input>
-        </div>
-        <div class="field-item field-value">
-          <div class="label">
-            <span class="required-symbol">*</span>
-            <span>Field Value</span>
-          </div>
-          <el-radio-group v-model="newFieldInfo.fieldType" @change="handleFieldTypeChange">
-            <el-radio label="Blank">Blank</el-radio>
-            <el-radio label="Fixed Value">Fixed Value</el-radio>
-          </el-radio-group>
-        </div>
-        <div class="field-item" v-if="newFieldInfo.fieldType === 'Fixed Value'">
-          <div class="label">
-            <span class="required-symbol">*</span>
-            <span>Fixed Value</span>
-          </div>
-          <el-input placeholder="Please enter..." v-model="newFieldInfo.value"></el-input>
-        </div>
-      </div>
-      <template #footer>
-        <el-button
-          style="height: 40px; width: 115px"
-          class="cancel-btn"
-          type="default"
-          @click="addNewFieldVisible = false"
-          >Cancel</el-button
-        >
-        <el-button style="height: 40px; width: 120px" class="el-button--dark" @click="addNewField"
-          >Apply</el-button
-        >
-      </template>
-    </el-dialog>
   </div>
 </template>
 
 <style lang="scss" scoped>
-.Title {
+.title {
   position: sticky;
   top: 0;
   z-index: 100;
@@ -605,12 +347,6 @@ const handlePageSave = () => {
   align-items: center;
   background-color: var(--color-mode);
 }
-.heaer_top {
-  margin-top: 6.57px;
-  margin-bottom: 8px;
-  padding-right: 8px;
-  display: flex;
-}
 
 .display {
   max-height: calc(100vh - 140px);
@@ -644,105 +380,7 @@ const handlePageSave = () => {
     padding: 8px 16px 16px;
   }
 }
-.fields-configuration {
-  div.content-box {
-    display: flex;
-    align-items: flex-start;
-    justify-content: center;
-    min-height: 272px;
-    max-height: 400px;
-    width: 100%;
-    padding-bottom: 8px;
-    padding-right: 0px;
-    // overflow: auto;
-    .empty-box {
-      align-self: center;
-      width: 100%;
-      text-align: center;
-      p {
-        margin-top: 12px;
-        color: var(--color-neutral-2);
-      }
-    }
-    .fields-list {
-      width: 100%;
-      max-height: 400px;
-      padding: 8px 0;
-      padding-right: 16px;
-      overflow: auto;
-      user-select: none;
-      .field-item {
-        display: flex;
-        align-items: center;
-        height: 48px;
-        margin-bottom: 8px;
-        padding: 0 16px;
-        border-radius: 6px;
-        border: 1px solid var(--color-border);
-        .label {
-          flex: 1;
-          .required-symbol {
-            color: var(--color-danger);
-          }
-        }
-        .display-name {
-          flex: 1.2;
-          margin: 0 16px;
-          :deep(.el-input__wrapper) {
-            height: 32px;
-          }
-        }
-        .actions {
-          display: flex;
-          align-items: center;
-          justify-content: space-between;
-          width: 240px;
-          padding-left: 30px;
-          .checkbox-group {
-            display: flex;
-          }
-          .el-checkbox {
-            margin-right: 16px;
-            :deep(.el-checkbox__inner) {
-              height: 16px;
-              width: 16px;
-              &::after {
-                border-width: 2px;
-                height: 9px;
-                width: 5px;
-                left: 4px;
-              }
-            }
-            :deep(.el-checkbox__label) {
-              margin-top: 3px;
-              padding-left: 4px;
-              line-height: 2;
-            }
-          }
-          .font_family {
-            float: right;
-            cursor: pointer;
-          }
-        }
-      }
-    }
-  }
-  .ghost-column {
-    cursor: move !important;
-    span {
-      opacity: 0;
-    }
-    border: 1px dashed var(--color-customize-column-item-drag-border) !important;
-    background-color: var(--color-customize-column-item-drag-bg) !important;
-    box-shadow: none !important;
-  }
 
-  .fallback-class {
-    opacity: 1 !important;
-    background-color: var(--color-customize-column-item-hover-bg) !important;
-    cursor: move !important;
-  }
-}
 .basic-info {
   .info-item {
     &:first-child {
@@ -817,9 +455,6 @@ const handlePageSave = () => {
         }
       }
     }
-    // .radio-item.specific-roles {
-    //   padding: 0;
-    // }
   }
 }
 
@@ -834,37 +469,3 @@ const handlePageSave = () => {
   }
 }
 </style>
-<style lang="scss">
-.add-new-field-dialog {
-  .field-item {
-    margin-bottom: 16px;
-    .label {
-      margin-bottom: 4px;
-    }
-    &:last-child {
-      margin-bottom: 0;
-    }
-  }
-  .field-value {
-    .el-radio-group {
-      width: 100%;
-      .el-radio {
-        flex: 1;
-        margin-right: 0;
-        padding-left: 12px;
-        border: 1px solid var(--color-border);
-        &:first-child {
-          border-radius: 6px 0 0 6px;
-          border-right: none;
-        }
-        &:last-child {
-          border-radius: 0 6px 6px 0;
-        }
-        .el-radio__label {
-          color: var(--color-neutral-1);
-        }
-      }
-    }
-  }
-}
-</style>

+ 4 - 1
src/views/TemplateManagement/src/components/CreateReportTemplate/src/components/AccountSelect.vue

@@ -34,7 +34,9 @@ interface ListItem {
 const options = ref<ListItem[]>([])
 const loading = ref(false)
 const currentController = ref<AbortController | null>(null)
-
+const handleVisibleChange = (visible) => {
+  !visible && (options.value = [])
+}
 const remoteMethod = (query: string) => {
   currentController.value?.abort()
 
@@ -89,6 +91,7 @@ onUnmounted(() => {
     popper-class="part-id-select-popper"
     :filter-method="debouncedRemoteMethod"
     @change="changeData"
+    @visible-change="handleVisibleChange"
   >
     <el-option v-for="item in options" :key="item.id" :label="item.label" :value="item.label">
       <div class="select-option">

+ 8 - 4
src/views/TemplateManagement/src/components/CreateReportTemplate/src/components/AdjustmentField.vue

@@ -102,6 +102,9 @@ const scrollToItem = (itemId: string) => {
       // container.scrollTop = targetElement.offsetTop - container.offsetTop
 
       document.addEventListener('click', handleDocumentClick)
+      setTimeout(() => {
+        searchColumn.value = ''
+      }, 600)
     }
   }, 100)
 }
@@ -137,8 +140,8 @@ const loading = ref(false)
 // 获取数据
 const getData = async (selectedList?: Array<any>) => {
   loading.value = true
-  let paramsData: any = { ...params.value }
-
+  let defaultValue = selectedList?.length && selectedList.length !== 0 ? false : true
+  let paramsData: any = { ...params.value, default: defaultValue }
   await $api.getReportFieldsConfiguration(paramsData).then((res: any) => {
     if (res.code === 200) {
       // allDataCopy就是所有的数据
@@ -150,6 +153,7 @@ const getData = async (selectedList?: Array<any>) => {
       // 右侧 当没有初始值时才需要从接口获取数据
       if (selectedList?.length && selectedList.length !== 0) {
         const newArr = selectedList.map((item: any) => {
+          handleAddSelect(item, false)
           return {
             ...item,
             label: item.title
@@ -206,7 +210,7 @@ const hoverAllIcon = ref('')
 // 右侧Icon的显隐
 const hoverSelectIcon = ref('')
 
-const handleAddSelect = (item: any) => {
+const handleAddSelect = (item: any, isNeedAdd = true) => {
   groupColumns.value.forEach((groupItem: any) => {
     groupItem.children.forEach((child: any, index: number) => {
       if (child.field === item.field) {
@@ -214,7 +218,7 @@ const handleAddSelect = (item: any) => {
       }
     })
   })
-  selectColumns.value.push(item)
+  isNeedAdd && selectColumns.value.push(item)
 }
 
 // 从左侧拖拽到右侧时,删除其他分组中相同的数据

+ 4 - 8
src/views/TemplateManagement/src/components/CreateReportTemplate/src/components/PartyIDSelect.vue

@@ -35,6 +35,9 @@ const options = ref<ListItem[]>([])
 const loading = ref(false)
 const currentController = ref<AbortController | null>(null)
 
+const handleVisibleChange = (visible) => {
+  !visible && (options.value = [])
+}
 const remoteMethod = (query: string) => {
   currentController.value?.abort()
 
@@ -89,6 +92,7 @@ onUnmounted(() => {
     popper-class="part-id-select-popper"
     :filter-method="debouncedRemoteMethod"
     @change="changeData"
+    @visible-change="handleVisibleChange"
   >
     <el-option
       v-for="item in options"
@@ -139,11 +143,3 @@ onUnmounted(() => {
   overflow: hidden;
 }
 </style>
-
-<style lang="scss">
-.part-id-select-popper {
-  // width: 100% !important;
-  // width: auto;
-  // min-width: unset !important;
-}
-</style>

+ 733 - 0
src/views/TemplateManagement/src/components/CreateReportTemplate/src/components/ReportFieldsConfiguration.vue

@@ -0,0 +1,733 @@
+<script setup lang="ts">
+import AdjustmentField from './AdjustmentField.vue'
+import { VueDraggable } from 'vue-draggable-plus'
+import { cloneDeep } from 'lodash'
+
+const props = defineProps<{
+  reportLevel: string
+  pageData
+}>()
+
+interface Field {
+  uniqueId: string
+  field: string
+  title: string
+  displayName: string
+  fieldType: string
+  value?: string
+  isFilter: boolean
+  fieldLevel?: string
+  isSort: boolean
+  mapping?: { system: string; converted: string }[]
+  groupName: string
+  isFieldDataMapping?: string
+}
+const fieldsList = ref<Field[]>([])
+const addNewFieldVisible = ref(false)
+const fieldLoading = ref(false)
+
+const newFieldInfo = ref<{
+  name: string
+  fieldType: 'Blank' | 'Fixed Value'
+  value: string
+}>({
+  name: '',
+  fieldType: 'Blank',
+  value: ''
+})
+
+watch(
+  () => props.pageData,
+  (newVal) => {
+    if (newVal && Array.isArray(newVal)) {
+      fieldsList.value = cloneDeep(newVal)
+    } else {
+      fieldsList.value = []
+    }
+  },
+  { immediate: true, deep: true }
+)
+
+const emit = defineEmits<{
+  (e: 'cancelChangeLevel', value: string): void
+  (e: 'confirmChangeLevel', value: string): void
+}>()
+
+const changeReportLevel = (newVal: string, oldVal: string) => {
+  // 定义每种切换场景的配置
+  const scenarios: Record<string, Record<string, { message: string; levelsToClear: string[] }>> = {
+    'Item Level': {
+      'Container Level': {
+        message: 'If the Level value is "Item Level", the field will be cleared.',
+        levelsToClear: ['Item Level']
+      },
+      'Shipment Level': {
+        message:
+          'If the Level value is "Item Level" or "Container Level", the field will be cleared.',
+        levelsToClear: ['Item Level', 'Container Level']
+      }
+    },
+    'Container Level': {
+      'Shipment Level': {
+        message: 'If the Level value is "Container Level", the field will be cleared.',
+        levelsToClear: ['Container Level']
+      }
+    }
+  }
+
+  const scenario = scenarios[oldVal]?.[newVal]
+
+  if (!scenario) {
+    // 无特殊清理逻辑,直接确认变更
+    emit('confirmChangeLevel', newVal)
+    return
+  }
+
+  ElMessageBox.confirm(scenario.message, 'Prompt', {
+    confirmButtonText: 'Change',
+    cancelButtonText: 'Cancel',
+    type: 'warning',
+    confirmButtonClass: 'el-button--dark',
+    cancelButtonClass: 'el-button--default',
+    distinguishCancelAndClose: true
+  })
+    .then(() => {
+      // 用户确认:过滤字段并提交新值
+      fieldsList.value = fieldsList.value.filter(
+        (item) => !scenario.levelsToClear.includes(item.fieldLevel)
+      )
+      emit('confirmChangeLevel', newVal)
+    })
+    .catch(() => {
+      // 用户取消:回滚到旧值
+      emit('cancelChangeLevel', oldVal)
+    })
+}
+
+const handleAddNewField = () => {
+  addNewFieldVisible.value = true
+}
+const handleFieldTypeChange = () => {
+  newFieldInfo.value.value = ''
+}
+const addNewField = () => {
+  if (!newFieldInfo.value.name?.trim()) {
+    ElMessage.warning('Please enter the new field name.')
+    return
+  }
+
+  if (newFieldInfo.value.fieldType === 'Fixed Value' && !newFieldInfo.value.value?.trim()) {
+    ElMessage.warning('Please enter the fixed value.')
+    return
+  }
+  fieldsList.value.unshift({
+    uniqueId: generate8DigitUnique(),
+    field: newFieldInfo.value.name,
+    title: newFieldInfo.value.name,
+    displayName: newFieldInfo.value.name,
+    value: newFieldInfo.value.value,
+    fieldType: 'Custom',
+    groupName: '',
+    isFilter: false,
+    isSort: false
+  })
+  clearNewFieldData()
+  addNewFieldVisible.value = false
+}
+const clearNewFieldData = () => {
+  newFieldInfo.value = {
+    name: '',
+    fieldType: 'Blank',
+    value: ''
+  }
+}
+
+const changeFieldConfig = (state: any, field: string, index: number, key: string) => {
+  if (!state) return
+  fieldsList.value.forEach((item) => {
+    if (item.field === field) {
+      item[key] = false
+    }
+  })
+  const firstMatch = fieldsList.value[index]
+  if (firstMatch) {
+    firstMatch[key] = true
+  }
+}
+const AdjustmentFieldRef = ref()
+// 打开定制表格弹窗
+const handleCustomizeColumns = () => {
+  if (!props.reportLevel) {
+    ElMessage.warning('Please select the report level.')
+    return
+  }
+  const params = {
+    serial_no: '',
+    level: props.reportLevel
+  }
+  const seen = new Set()
+  const uniqueArray = fieldsList.value.filter((item) => {
+    if (seen.has(item.field)) {
+      return false // 已存在,跳过
+    }
+    seen.add(item.field)
+    return true // 第一次出现,保留
+  })
+
+  AdjustmentFieldRef.value.openDialog(
+    params,
+    -220,
+    'Drag item over to this selection or click "add" icon to show the field on report template list',
+    uniqueArray
+  )
+}
+
+const fieldMappingVisible = ref(false)
+const curFieldItem = ref()
+const mappingData = ref([
+  {
+    system: '',
+    converted: ''
+  }
+])
+const copyFieldsList = ref({})
+
+const handleMappingField = (index: number) => {
+  curFieldItem.value = fieldsList.value[index]
+  mappingData.value =
+    curFieldItem.value.mapping && curFieldItem.value.mapping.length
+      ? cloneDeep(curFieldItem.value.mapping)
+      : [
+          {
+            system: '',
+            converted: ''
+          }
+        ]
+  fieldMappingVisible.value = true
+}
+const handleDeleteField = (index: number, uniqueId: string) => {
+  fieldsList.value.splice(index, 1)
+
+  Object.keys(copyFieldsList.value).forEach((key) => {
+    copyFieldsList.value[key] = copyFieldsList.value[key].filter(
+      (item) => item.uniqueId !== uniqueId
+    )
+  })
+}
+
+// 前端使用的唯一标识符
+const generate8DigitUnique = () => {
+  return Math.floor(10000000 + Math.random() * 90000000).toString()
+}
+const handleCopyField = (index: number) => {
+  const curField = cloneDeep(fieldsList.value[index])
+  curField.displayName = `${curField.displayName} (Copy)`
+  curField.isFilter = false
+  curField.isSort = false
+  curField.uniqueId = generate8DigitUnique()
+  if (copyFieldsList.value[curField.field]) {
+    copyFieldsList.value[curField.field].push(cloneDeep(curField))
+  } else {
+    copyFieldsList.value[curField.field] = [cloneDeep(curField)]
+  }
+  fieldsList.value.splice(index + 1, 0, cloneDeep(curField))
+}
+
+const handleAddMapping = () => {
+  curFieldItem.value.mapping = mappingData.value
+  clearMappingData()
+  fieldMappingVisible.value = false
+}
+const handleAddMappingItem = () => {
+  mappingData.value.push({
+    system: '',
+    converted: ''
+  })
+}
+const clearMappingData = () => {
+  mappingData.value = [
+    {
+      system: '',
+      converted: ''
+    }
+  ]
+}
+
+// 调整应用字段
+const handleApplay = (data: any) => {
+  const customizeData = fieldsList.value.filter((item: any) => item.field_type === 'Custom')
+  fieldsList.value = data.map((item: any) => {
+    return {
+      ...item,
+      label: item.label,
+      title: item.title || item.label,
+      displayName: item.displayName || item.label,
+      isFilter: !!item.isFilter,
+      isSort: !!item.isSort,
+      uniqueId: item.uniqueId || generate8DigitUnique()
+    }
+  })
+  fieldsList.value = [...customizeData, ...fieldsList.value]
+
+  const validFields = new Set(fieldsList.value.map((item) => item.field))
+  for (const field in copyFieldsList.value) {
+    if (!validFields.has(field)) {
+      delete copyFieldsList.value[field]
+    }
+  }
+
+  // === 第三步:从后往前插入副本(不修改原始项)===
+  for (let i = fieldsList.value.length - 1; i >= 0; i--) {
+    const field = fieldsList.value[i].field
+    const copies = copyFieldsList.value[field]
+
+    if (copies?.length) {
+      // 插入副本到原项后面
+      fieldsList.value.splice(i + 1, 0, ...copies)
+    }
+  }
+}
+
+const handleDeleteMappingField = (index: number) => {
+  mappingData.value.splice(index, 1)
+}
+const getFieldsList = () => {
+  return fieldsList.value || []
+}
+
+defineExpose({
+  getFieldsList,
+  changeReportLevel
+})
+</script>
+
+<template>
+  <div class="fields-configuration template-box">
+    <div class="header">
+      <span>Report Fields Configuration</span>
+
+      <div class="right-option">
+        <el-button
+          class="el-button--dark"
+          @click="handleAddNewField"
+          style="width: 148px; padding-top: 11px"
+        >
+          <span style="margin-right: 3px" class="font_family icon-icon_add_b"></span>
+          <span>Add New Field</span>
+        </el-button>
+        <el-button
+          v-if="fieldsList.length > 0"
+          class="el-button--dark"
+          @click="handleCustomizeColumns()"
+          style="width: 110px; padding-top: 11px"
+        >
+          <span style="margin-right: 3px" class="font_family icon-icon_add_b"></span>
+          <span>Select Field</span>
+        </el-button>
+      </div>
+    </div>
+    <div class="content-box">
+      <div class="empty-box" v-if="fieldsList.length === 0">
+        <el-button class="el-button--dark" @click="handleCustomizeColumns">
+          <span class="font_family icon-icon_add_b"></span>Add/Edit Field
+        </el-button>
+        <p>No field selected. click “Add Field” to get started.</p>
+      </div>
+      <div class="fields-list" v-else>
+        <VueDraggable
+          v-vloading="fieldLoading"
+          v-model="fieldsList"
+          class="column-list"
+          ghost-class="ghost-column"
+          :forceFallback="true"
+          fallback-class="fallback-class"
+          group="customizeColumns"
+          item-key="uniqueId"
+          handle=".handle-draggable"
+        >
+          <div
+            class="field-item"
+            v-for="(fieldItem, index) in fieldsList"
+            :key="fieldItem.uniqueId"
+          >
+            <span
+              class="font_family icon-icon_dragsort__b draggable-icon handle-draggable"
+              style="margin-right: 12px; font-size: 16px"
+            ></span>
+            <div class="label handle-draggable" style="line-height: 2.7">
+              <span style="font-weight: 700">[{{ fieldItem.field }}]</span>
+              <span style="margin-left: 8px">{{ fieldItem.title }}</span>
+            </div>
+            <el-input
+              :id="fieldItem.uniqueId"
+              :name="fieldItem.uniqueId"
+              class="display-name"
+              v-model="fieldItem.displayName"
+              placeholder="Display Name in Report"
+            ></el-input>
+            <div class="actions">
+              <div class="checkbox-group">
+                <el-checkbox
+                  :disabled="
+                    fieldItem.fieldType !== 'System' ||
+                    fieldItem.groupName === 'Container Status' ||
+                    fieldItem.groupName === 'Milestone'
+                  "
+                  v-model="fieldItem.isFilter"
+                  @change="changeFieldConfig($event, fieldItem.field, index, 'isFilter')"
+                  >Filter</el-checkbox
+                >
+                <el-checkbox
+                  :disabled="fieldItem.fieldType !== 'System'"
+                  v-model="fieldItem.isSort"
+                  @change="changeFieldConfig($event, fieldItem.field, index, 'isSort')"
+                  >Sort</el-checkbox
+                >
+              </div>
+              <div class="action-icon">
+                <span
+                  style="padding: 5px"
+                  v-if="fieldItem.fieldType === 'System' && fieldItem.isFieldDataMapping === 't'"
+                  @click="handleMappingField(index)"
+                  class="font_family icon-icon_convert_b"
+                ></span>
+                <span v-else style="width: 20px"></span>
+                <span
+                  style="padding: 5px"
+                  @click="handleCopyField(index)"
+                  class="font_family icon-icon_clone_b"
+                ></span>
+                <span
+                  style="padding: 5px 7px"
+                  @click="handleDeleteField(index, fieldItem.uniqueId)"
+                  class="font_family icon-icon_delete_b"
+                ></span>
+              </div>
+            </div>
+          </div>
+        </VueDraggable>
+      </div>
+    </div>
+  </div>
+
+  <AdjustmentField @apply="handleApplay" ref="AdjustmentFieldRef" />
+  <el-dialog
+    class="add-new-field-dialog"
+    title="Add New Field"
+    v-model="addNewFieldVisible"
+    @closed="clearNewFieldData"
+    width="480"
+  >
+    <div>
+      <div class="field-item">
+        <div class="label">
+          <span class="required-symbol">*</span>
+          <span>New Field Name</span>
+        </div>
+        <el-input placeholder="Please enter..." v-model="newFieldInfo.name"></el-input>
+      </div>
+      <div class="field-item field-value">
+        <div class="label">
+          <span class="required-symbol">*</span>
+          <span>Field Value</span>
+        </div>
+        <el-radio-group v-model="newFieldInfo.fieldType" @change="handleFieldTypeChange">
+          <el-radio label="Blank">Blank</el-radio>
+          <el-radio label="Fixed Value">Fixed Value</el-radio>
+        </el-radio-group>
+      </div>
+      <div class="field-item" v-if="newFieldInfo.fieldType === 'Fixed Value'">
+        <div class="label">
+          <span class="required-symbol">*</span>
+          <span>Fixed Value</span>
+        </div>
+        <el-input placeholder="Please enter..." v-model="newFieldInfo.value"></el-input>
+      </div>
+    </div>
+    <template #footer>
+      <el-button
+        style="height: 40px; width: 115px"
+        class="cancel-btn"
+        type="default"
+        @click="addNewFieldVisible = false"
+        >Cancel</el-button
+      >
+      <el-button style="height: 40px; width: 120px" class="el-button--dark" @click="addNewField"
+        >Apply</el-button
+      >
+    </template>
+  </el-dialog>
+  <el-dialog
+    class="field-mapping-dialog"
+    v-model="fieldMappingVisible"
+    title="Mapping (Field: Final Destination)"
+    width="800"
+    @close="clearMappingData"
+  >
+    <div>
+      <div class="mapping-list">
+        <div class="label">
+          <div class="left-label">
+            <span style="color: var(--color-danger)">*</span>
+            <span>System Value</span>
+          </div>
+          <div class="right-label">
+            <span style="color: var(--color-danger)">*</span>
+            <span>Output Value</span>
+          </div>
+        </div>
+        <div style="max-height: 380px; overflow-y: auto">
+          <div class="list-item" v-for="(item, index) in mappingData" :key="index">
+            <div class="left-system-value">
+              <el-input placeholder="Please enter..." clearable v-model="item.system"></el-input>
+            </div>
+            <div class="right-converted-value">
+              <el-input placeholder="Please enter..." clearable v-model="item.converted"></el-input>
+            </div>
+            <div class="delete-icon">
+              <span
+                @click="handleDeleteMappingField(index)"
+                class="font_family icon-icon_delete_b"
+              ></span>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+    <el-button
+      class="el-button--text"
+      @click="handleAddMappingItem"
+      style="height: 32px; margin-top: 8px; padding: 8px"
+    >
+      <span class="font_family icon-icon_add_b" style="color: var(--color-theme)"></span>
+      <span style="color: var(--color-theme)">Add Mapping</span>
+    </el-button>
+    <template #footer>
+      <el-button
+        style="height: 40px; width: 115px"
+        class="cancel-btn"
+        type="default"
+        @click="fieldMappingVisible = false"
+        >Cancel</el-button
+      >
+      <el-button
+        style="height: 40px; width: 120px"
+        class="el-button--dark"
+        @click="handleAddMapping"
+        >Apply</el-button
+      >
+    </template>
+  </el-dialog>
+</template>
+
+<style lang="scss" scoped>
+.template-box {
+  margin-bottom: 8px;
+  border: 1px solid var(--color-border);
+  border-radius: 12px;
+  overflow: hidden;
+  .header {
+    height: 48px;
+    border-bottom: 1px solid var(--color-border);
+    display: flex;
+    align-items: center;
+    padding: 0 16px;
+    border-radius: 12px 12px 0 0;
+    background-color: var(--color-header-bg);
+    font-size: 18px;
+    font-weight: bold;
+  }
+  .right-option {
+    margin-left: auto;
+  }
+  .content-box {
+    height: 100%;
+    padding: 8px 16px 16px;
+  }
+}
+.fields-configuration {
+  div.content-box {
+    display: flex;
+    align-items: flex-start;
+    justify-content: center;
+    min-height: 272px;
+    max-height: 400px;
+    width: 100%;
+    padding-bottom: 8px;
+    padding-right: 0px;
+    // overflow: auto;
+    .empty-box {
+      align-self: center;
+      width: 100%;
+      text-align: center;
+      p {
+        margin-top: 12px;
+        color: var(--color-neutral-2);
+      }
+    }
+    .fields-list {
+      width: 100%;
+      max-height: 400px;
+      padding: 8px 0;
+      padding-right: 16px;
+      overflow: auto;
+      user-select: none;
+      .field-item {
+        display: flex;
+        align-items: center;
+        height: 48px;
+        margin-bottom: 8px;
+        padding-left: 16px;
+        border-radius: 6px;
+        border: 1px solid var(--color-border);
+        .label {
+          flex: 1;
+          .required-symbol {
+            color: var(--color-danger);
+          }
+        }
+        .display-name {
+          flex: 1.2;
+          margin: 0 16px;
+          :deep(.el-input__wrapper) {
+            height: 32px;
+          }
+        }
+        .actions {
+          display: flex;
+          align-items: center;
+          justify-content: space-between;
+          width: 303px;
+          padding-left: 30px;
+          .checkbox-group {
+            display: flex;
+          }
+          .el-checkbox {
+            margin-right: 16px;
+            :deep(.el-checkbox__inner) {
+              height: 16px;
+              width: 16px;
+              &::after {
+                border-width: 2px;
+                height: 9px;
+                width: 5px;
+                left: 4px;
+              }
+            }
+            :deep(.el-checkbox__label) {
+              margin-top: 3px;
+              padding-left: 4px;
+              line-height: 2;
+            }
+          }
+          .font_family {
+            float: right;
+            cursor: pointer;
+          }
+          .action-icon {
+            width: 144px;
+            display: flex;
+            justify-content: space-around;
+          }
+        }
+      }
+    }
+  }
+  .ghost-column {
+    cursor: move !important;
+    span {
+      opacity: 0;
+    }
+    border: 1px dashed var(--color-customize-column-item-drag-border) !important;
+    background-color: var(--color-customize-column-item-drag-bg) !important;
+    box-shadow: none !important;
+  }
+
+  .fallback-class {
+    opacity: 1 !important;
+    background-color: var(--color-customize-column-item-hover-bg) !important;
+    cursor: move !important;
+  }
+}
+</style>
+<style lang="scss">
+.add-new-field-dialog {
+  .field-item {
+    margin-bottom: 16px;
+    .label {
+      margin-bottom: 4px;
+    }
+    &:last-child {
+      margin-bottom: 0;
+    }
+  }
+  .field-value {
+    .el-radio-group {
+      width: 100%;
+      .el-radio {
+        flex: 1;
+        margin-right: 0;
+        padding-left: 12px;
+        border: 1px solid var(--color-border);
+        &:first-child {
+          border-radius: 6px 0 0 6px;
+          border-right: none;
+        }
+        &:last-child {
+          border-radius: 0 6px 6px 0;
+        }
+        .el-radio__label {
+          color: var(--color-neutral-1);
+        }
+      }
+    }
+  }
+}
+.field-mapping-dialog {
+  .mapping-list {
+    border-radius: 6px;
+    overflow: hidden;
+    .label {
+      display: flex;
+      height: 24px;
+      width: 768px;
+      background-color: var(--color-header-bg);
+      .left-label {
+        width: 364px;
+        padding: 4px 8px;
+      }
+      .right-label {
+        width: 404px;
+        padding: 4px 8px;
+      }
+    }
+    .list-item {
+      display: flex;
+      height: 48px;
+      border: 1px solid var(--color-border);
+      overflow: hidden;
+
+      .left-system-value,
+      .right-converted-value {
+        flex: 1;
+        padding: 8px;
+      }
+      .left-system-value {
+        border-right: 1px solid var(--color-border);
+      }
+      .delete-icon {
+        width: 48px;
+        text-align: center;
+        line-height: 48px;
+      }
+      &:nth-child(n + 3) {
+        border-top: none;
+      }
+
+      &:last-child {
+        border-radius: 0 0 6px 6px;
+      }
+    }
+  }
+}
+</style>

+ 16 - 6
src/views/TemplateManagement/src/components/TableView/src/TableView.vue

@@ -5,7 +5,7 @@ import { useRowClickStyle } from '@/hooks/rowClickStyle'
 import dayjs from 'dayjs'
 import utc from 'dayjs/plugin/utc'
 import timezone from 'dayjs/plugin/timezone'
-import { formatNumber } from '@/utils/tools'
+import { formatNumber, formatTimezoneByUser } from '@/utils/tools'
 import { useRouter } from 'vue-router'
 import { useUserStore } from '@/stores/modules/user'
 
@@ -24,7 +24,6 @@ const formatString = computed(() => {
   return userStore.dateFormat || 'MM/DD/YYYY'
 })
 
-const tableOriginColumnsField = ref()
 const tableColumns = [
   {
     title: 'Report Name',
@@ -52,8 +51,20 @@ const tableColumns = [
     field: 'created_time',
     formatter: 'dateTime',
     sortable: true
+  },
+  {
+    title: 'Created By',
+    type: 'normal',
+    field: 'create_by'
+  },
+  {
+    title: 'Modify By',
+    type: 'normal',
+    field: 'modify_by'
   }
 ]
+const tableOriginColumnsField = ref([...tableColumns])
+
 const handleColumns = (columns: any) => {
   const newColumns = columns.map((item: any) => {
     let curColumn: any = {
@@ -84,11 +95,10 @@ const handleColumns = (columns: any) => {
       }
     }
     // 格式化
-    if (item.formatter === 'date' || item.formatter === 'dateTime') {
+    if (item.formatter === 'dateTime') {
       curColumn = {
         ...curColumn,
-        formatter: ({ cellValue }: any) =>
-          (dayjs as any).tz(cellValue, 'US/Pacific').format(formatString.value + ' HH:mm')
+        formatter: ({ cellValue }: any) => formatTimezoneByUser(cellValue, 'YYYY-MM-DD HH:mm', true)
       }
     }
     return curColumn
@@ -130,7 +140,7 @@ const assignTableData = (data: any) => {
     return item.title === 'Action'
   })
   actionColumn.width = isShowDeleteBtn.value ? 150 : 120
-  tableRef.value.loadColumn(tableData.value.columns)
+  tableRef.value.loadColumn(tableData.value.columns || [])
 }
 
 let searchdata: any = {}

+ 214 - 673
src/views/Tracking/src/TrackingView.vue

@@ -1,6 +1,5 @@
 <script setup lang="ts">
 import FilterTags from '@/components/FliterTags'
-import emitter from '@/utils/bus'
 import TransportMode from '@/components/TransportMode'
 import TrackingTable from './components/TrackingTable'
 import DateRange from '@/components/DateRange'
@@ -10,612 +9,123 @@ import { useCalculatingHeight } from '@/hooks/calculatingHeight'
 import { useHeaderSearch } from '@/stores/modules/headerSearch'
 import { useUserStore } from '@/stores/modules/user'
 import dayjs from 'dayjs'
+import { useFiltersStore } from '@/stores/modules/filtersList'
 
 const userStore = useUserStore()
 const formatDate = userStore.dateFormat
-const valueFormatDate = 'MM/DD/YYYY'
-
-const headerSearch = useHeaderSearch()
 
 const filterRef: Ref<HTMLElement | null> = ref(null)
-
 const containerHeight = useCalculatingHeight(document.documentElement, 246, [filterRef])
 
-const TrackingSearch = ref()
-const tableLoadingTableData = ref(false)
-let searchTableQeuryTracking: any = {}
-const filterData = reactive({
-  filtersTagData: [] as Array<string>,
-  transportData: [] as Array<string>,
-  daterangeData: [] as Array<string>,
-  morefiltersData: [] as Array<string>,
-  dashboardData: [] as Array<string>
-})
-const tagsData: any = ref([])
-const handleClose = (tag: any) => {
-  emitter.emit('clearTag', tag)
-  tagsData.value.splice(tagsData.value.indexOf(tag), 1)
-  if (
-    sessionStorage.getItem('reportList') != null ||
-    sessionStorage.getItem('reportList') != '{}'
-  ) {
-    const reportList = JSON.parse(sessionStorage.getItem('reportList') as string) || {}
-    let data = JSON.parse(sessionStorage.getItem('tagsList') as string) || {}
-    if (tag.includes('Transport')) {
-      delete reportList.transport_mode
-    } else if (tag.includes('Day') || tag.includes('CO2e')) {
-      delete reportList._reportRef
-      delete reportList._reportType
-      delete reportList._reportRefe_date
-      delete reportList._reportRefb_date
-      delete reportList._reportStationType
-      delete reportList._reportDataType
-      delete reportList._reportStationType
-      filterData.dashboardData = []
-      data = {}
-    } else if (tag.includes('ETD')) {
-      filterData.daterangeData.forEach((item: any, index: any) => {
-        if (item.includes('ETD')) {
-          filterData.daterangeData.splice(index, 1)
-        }
-      })
-      delete reportList.etd_start
-      delete reportList.etd_end
-    } else if (tag.includes('ETA')) {
-      filterData.daterangeData.forEach((item: any, index: any) => {
-        if (item.includes('ETA')) {
-          filterData.daterangeData.splice(index, 1)
-        }
-      })
-      delete reportList.eta_start
-      delete reportList.eta_end
-    } else if (tag.includes('Origin')) {
-      delete reportList.shipper_city
-      delete reportList._city_name
-      filterData.dashboardData = []
-    } else if (tag.includes('Destination')) {
-      delete reportList.consignee_city
-    }
-    sessionStorage.setItem('reportList', JSON.stringify(reportList))
-    sessionStorage.setItem('tagsList', JSON.stringify(data))
-  }
-  if (tag.includes('Transport')) {
-    delete searchTableQeuryTracking.transport_mode
-  } else if (tag.includes('Day') || tag.includes('CO2e')) {
-    delete searchTableQeuryTracking._reportRef
-    delete searchTableQeuryTracking._reportType
-    delete searchTableQeuryTracking._reportRefe_date
-    delete searchTableQeuryTracking._reportRefb_date
-    delete searchTableQeuryTracking._reportStationType
-    delete searchTableQeuryTracking._reportDataType
-    delete searchTableQeuryTracking._reportStationType
-    filterData.dashboardData = []
-  } else if (tag.includes('ETD')) {
-    filterData.daterangeData.forEach((item: any, index: any) => {
-      if (item.includes('ETD')) {
-        filterData.daterangeData.splice(index, 1)
-      }
-    })
-    delete searchTableQeuryTracking.etd_start
-    delete searchTableQeuryTracking.etd_end
-  } else if (tag.includes('ETA')) {
-    filterData.daterangeData.forEach((item: any, index: any) => {
-      if (item.includes('ETA')) {
-        filterData.daterangeData.splice(index, 1)
-      }
-    })
-    delete searchTableQeuryTracking.eta_start
-    delete searchTableQeuryTracking.eta_end
-  } else if (tag.includes('Creation')) {
-    delete searchTableQeuryTracking.created_time_start
-    delete searchTableQeuryTracking.created_time_end
-  } else if (tag.includes('Shippername')) {
-    delete searchTableQeuryTracking.shipper
-  } else if (tag.includes('Consigneename')) {
-    delete searchTableQeuryTracking.consignee
-  } else if (tag.includes('Service')) {
-    delete searchTableQeuryTracking.service
-  } else if (tag.includes('Incoterms')) {
-    delete searchTableQeuryTracking.incoterms
-  } else if (tag.includes('Notify Party')) {
-    delete searchTableQeuryTracking.notify_party
-  } else if (tag.includes('Bill to')) {
-    delete searchTableQeuryTracking.billto
-  } else if (tag.includes('Origin Agent')) {
-    delete searchTableQeuryTracking.origin
-  } else if (tag.includes('Destination Agent')) {
-    delete searchTableQeuryTracking.agent
-  } else if (tag.includes('Destination Operator')) {
-    delete searchTableQeuryTracking.dest_op
-  } else if (tag.includes('Sales')) {
-    delete searchTableQeuryTracking.sales_rep
-  } else if (tag.includes('Origin')) {
-    delete searchTableQeuryTracking.shipper_city
-    delete searchTableQeuryTracking._city_name
-    filterData.dashboardData = []
-  } else if (tag.includes('Destination')) {
-    delete searchTableQeuryTracking.consignee_city
-  } else if (tag.includes('Place of Receipt')) {
-    delete searchTableQeuryTracking.place_of_receipt_exp
-  } else if (tag.includes('Port of Loading')) {
-    delete searchTableQeuryTracking.port_of_loading
-  } else if (tag.includes('Place of delivery')) {
-    delete searchTableQeuryTracking.place_of_delivery_exp
-  } else if (tag.includes('Place of Discharge')) {
-    delete searchTableQeuryTracking.port_of_discharge
-  } else if (tag.includes('Vessel')) {
-    delete searchTableQeuryTracking['f_vessel/vessel']
-  } else if (tag.includes('Voyage')) {
-    delete searchTableQeuryTracking['f_voyage/voyage']
-  }
-
-  sessionStorage.setItem('searchTableQeuryTracking', JSON.stringify(searchTableQeuryTracking))
-  getTrackingData()
-}
-// 筛选框查询
-const FiltersSeach = (val: any, value: any) => {
-  searchTableQeuryTracking[val] = value
-
-  sessionStorage.setItem('searchTableQeuryTracking', JSON.stringify(searchTableQeuryTracking))
-  getTrackingData()
-}
-//TransportSearch
-const TransportSearch = (val: any) => {
-  filterData.transportData = []
-  if (val.data.length != 0) {
-    let str = `${val.title}:${val.data}`
-    filterData.transportData.push(str)
-  }
-  FiltersSeach('transport_mode', val.data)
-  renderTagsData()
-}
-// defaultTransport
-const defaultTransport = (val: any, value: any) => {
-  filterData.transportData = []
-  if (val.data.length != 0) {
-    let str = `${val.title}:${val.data}`
-    filterData.transportData.push(str)
-  }
-  if (sessionStorage.getItem('searchTableQeuryTracking') == null) {
-    if (
-      sessionStorage.getItem('clickParams') === null ||
-      sessionStorage.getItem('clickParams') === '{}'
-    ) {
-      searchTableQeuryTracking.transport_mode = val.data
-    }
-  } else {
-    searchTableQeuryTracking = value
-  }
-}
-const setFilterData = (dateRangeData: any) => {
-  filterData.daterangeData = []
-  for (const key in dateRangeData) {
-    const startEnd = dateRangeData[key].data[0] + ' To ' + dateRangeData[key].data[1]
-    let str = `${key}:${startEnd}`
-    filterData.daterangeData.push(str)
-  }
-}
-//defaultDate
-const defaultDate = (dateRangeData: any, data: any) => {
-  setFilterData(dateRangeData)
-
-  if (sessionStorage.getItem('searchTableQeuryTracking') == null) {
-    if (
-      sessionStorage.getItem('clickParams') === null ||
-      sessionStorage.getItem('clickParams') === '{}'
-    ) {
-      if (Object.keys(dateRangeData).length == 0) {
-        delete searchTableQeuryTracking.etd_start
-        delete searchTableQeuryTracking.etd_end
-      }
-      for (const key in dateRangeData) {
-        searchTableQeuryTracking.etd_start = dateRangeData[key].data[0]
-
-        searchTableQeuryTracking.etd_end = dateRangeData[key].data[1]
-      }
-    }
-  } else {
-    searchTableQeuryTracking = data
-    if (searchTableQeuryTracking._textSearch) {
-      TrackingSearch.value = searchTableQeuryTracking._textSearch
-    }
-  }
-
-  const rawData = localStorage.getItem('searchData')
-  const trackingData = rawData ? JSON.parse(rawData) : null
-  if (trackingData) {
-    // 根据顶部搜索框的搜索结果赋值
-    initDataByHeaderSearch()
-  } else if (!trackingData && !sessionStorage.getItem('clickParams')) {
-    getTrackingData()
-  }
-
-  renderTagsData()
-}
-//DateRangeSearch
-const DateRangeSearch = (dateRangeData: any) => {
-  setFilterData(dateRangeData)
-  if (Object.keys(dateRangeData).length == 0) {
-    delete searchTableQeuryTracking.etd_start
-    delete searchTableQeuryTracking.etd_end
-    delete searchTableQeuryTracking.eta_start
-    delete searchTableQeuryTracking.eta_end
-    delete searchTableQeuryTracking.created_time_start
-    delete searchTableQeuryTracking.created_time_end
-  }
-  const fieldList = [
-    {
-      title: 'ETD',
-      keys: ['etd_start', 'etd_end']
-    },
-    { title: 'ETA', keys: ['eta_start', 'eta_end'] },
-    { title: 'Creation Time', keys: ['created_time_start', 'created_time_end'] }
-  ]
-  fieldList.forEach((item) => {
-    if (!dateRangeData.hasOwnProperty(item.title)) {
-      // 删除不存在的字段
-      searchTableQeuryTracking[item.keys[0]] = undefined
-      searchTableQeuryTracking[item.keys[1]] = undefined
-    }
-  })
-  for (const key in dateRangeData) {
-    if (key == 'ETD') {
-      searchTableQeuryTracking.etd_start = dateRangeData[key].data[0]
-      searchTableQeuryTracking.etd_end = dateRangeData[key].data[1]
-    } else if (key == 'ETA') {
-      searchTableQeuryTracking.eta_start = dateRangeData[key].data[0]
-      searchTableQeuryTracking.eta_end = dateRangeData[key].data[1]
-    } else {
-      searchTableQeuryTracking.created_time_start = dateRangeData[key].data[0]
-      searchTableQeuryTracking.created_time_end = dateRangeData[key].data[1]
-    }
-  }
-  sessionStorage.setItem('searchTableQeuryTracking', JSON.stringify(searchTableQeuryTracking))
-  getTrackingData()
-  renderTagsData()
-}
-//MoreFiltersSearch
-const MoreFiltersSearch = (val: any, value: any) => {
-  filterData.morefiltersData = []
-  if (Object.keys(value).length == 0) {
-    delete searchTableQeuryTracking.shipper
-    delete searchTableQeuryTracking.consignee
-    delete searchTableQeuryTracking.service
-    delete searchTableQeuryTracking.incoterms
-    delete searchTableQeuryTracking.notify_party
-    delete searchTableQeuryTracking.billto
-    delete searchTableQeuryTracking.origin
-    delete searchTableQeuryTracking.agent
-    delete searchTableQeuryTracking.sales_rep
-    delete searchTableQeuryTracking.dest_op
-    delete searchTableQeuryTracking.consignee_city
-    delete searchTableQeuryTracking.place_of_receipt_exp
-    delete searchTableQeuryTracking.place_of_delivery_exp
-    delete searchTableQeuryTracking.port_of_discharge
-    delete searchTableQeuryTracking.shipper_city
-    delete searchTableQeuryTracking['port_of_loading/fport_of_loading_un']
-    delete searchTableQeuryTracking['f_vessel/vessel']
-    delete searchTableQeuryTracking['f_voyage/voyage']
-  }
-  for (const key in val) {
-    let str = `${key}:${val[key]}`
-    filterData.morefiltersData.push(str)
-    if (key == 'Shippername') {
-      searchTableQeuryTracking.shipper = value[key]
-    } else if (key == 'Consigneename') {
-      searchTableQeuryTracking.consignee = value[key]
-    } else if (key == 'Service') {
-      searchTableQeuryTracking.service = value[key]
-    } else if (key == 'Incoterms') {
-      searchTableQeuryTracking.incoterms = value[key]
-    } else if (key == 'Notify Party') {
-      searchTableQeuryTracking.notify_party = value[key]
-    } else if (key == 'Bill to') {
-      searchTableQeuryTracking.billto = value[key]
-    } else if (key == 'Origin Agent') {
-      searchTableQeuryTracking.origin = value[key]
-    } else if (key == 'Destination Agent') {
-      searchTableQeuryTracking.agent = value[key]
-    } else if (key == 'Destination Operator') {
-      searchTableQeuryTracking.dest_op = value[key]
-    } else if (key == 'Sales') {
-      searchTableQeuryTracking.sales_rep = value[key]
-    } else if (key == 'Destination') {
-      searchTableQeuryTracking.consignee_city = value[key]
-    } else if (key == 'Place of Receipt') {
-      searchTableQeuryTracking.place_of_receipt_exp = value[key]
-    } else if (key == 'Origin') {
-      searchTableQeuryTracking.shipper_city = value[key]
-    } else if (key == 'Port of Loading') {
-      searchTableQeuryTracking['port_of_loading/fport_of_loading_un'] = value[key]
-    } else if (key == 'Place of delivery') {
-      searchTableQeuryTracking.place_of_delivery_exp = value[key]
-    } else if (key == 'Port of Discharge') {
-      searchTableQeuryTracking.port_of_discharge = value[key]
-    } else if (key == 'Vessel') {
-      searchTableQeuryTracking['f_vessel/vessel'] = value[key]
-    } else if (key == 'Voyage') {
-      searchTableQeuryTracking['f_voyage/voyage'] = value[key]
-    }
-  }
-  sessionStorage.setItem('searchTableQeuryTracking', JSON.stringify(searchTableQeuryTracking))
-  getTrackingData()
-  renderTagsData()
-}
-const defaultMorefilters = (val: any, value: any, data: any) => {
-  filterData.morefiltersData = []
-  for (const key in val) {
-    let str = `${key}:${val[key]}`
-    filterData.morefiltersData.push(str)
-    for (const key in val) {
-      if (key == 'Shippername') {
-        searchTableQeuryTracking.shipper = value[key]
-      } else if (key == 'Consigneename') {
-        searchTableQeuryTracking.consignee = value[key]
-      } else if (key == 'Service') {
-        searchTableQeuryTracking.service = value[key]
-      } else if (key == 'Incoterms') {
-        searchTableQeuryTracking.incoterms = value[key]
-      } else if (key == 'Notify Party') {
-        searchTableQeuryTracking.notify_party = value[key]
-      } else if (key == 'Bill to') {
-        searchTableQeuryTracking.billto = value[key]
-      } else if (key == 'Destination Operator') {
-        searchTableQeuryTracking.dest_op = value[key]
-      } else if (key == 'Origin Agent') {
-        searchTableQeuryTracking.origin = value[key]
-      } else if (key == 'Destination Agent') {
-        searchTableQeuryTracking.agent = value[key]
-      } else if (key == 'Sales') {
-        searchTableQeuryTracking.sales_rep = value[key]
-      } else if (key == 'Origin') {
-        searchTableQeuryTracking.shipper_city = value[key]
-      } else if (key == 'Destination') {
-        searchTableQeuryTracking.consignee_city = value[key]
-      } else if (key == 'Place of Receipt') {
-        searchTableQeuryTracking.place_of_receipt_exp = value[key]
-      } else if (key == 'Port of Loading') {
-        searchTableQeuryTracking.port_of_loading = value[key]
-      } else if (key == 'Place of delivery') {
-        searchTableQeuryTracking.place_of_delivery_exp = value[key]
-      } else if (key == 'Place of Discharge') {
-        searchTableQeuryTracking.port_of_discharge = value[key]
-      } else if (key == 'Vessel') {
-        searchTableQeuryTracking['f_vessel/vessel'] = value[key]
-      } else if (key == 'Voyage') {
-        searchTableQeuryTracking['f_voyage/voyage'] = value[key]
-      }
-    }
-    if (sessionStorage.getItem('searchTableQeuryTracking') != null) {
-      searchTableQeuryTracking = data
-    }
-  }
-}
-const clearfilters = () => {
-  TrackingSearch.value = ''
-  filterData.filtersTagData = []
-  tagsData.value = []
-  let str = 'Shipment status: All'
-  filterData.filtersTagData.push(str)
-  filterData.transportData = []
-  filterData.daterangeData = []
-  filterData.morefiltersData = []
-  filterData.dashboardData = []
-  emitter.emit('clearTag', 'Shipment status')
-  emitter.emit('clearTag', 'Transport Mode')
-  emitter.emit('clearTag', 'ETD')
-  emitter.emit('clearTag', 'ETA')
-  emitter.emit('clearTag', 'Creation Time')
-  emitter.emit('clearTag', 'Shippername')
-  emitter.emit('clearTag', 'Consigneename')
-  emitter.emit('clearTag', 'Service')
-  emitter.emit('clearTag', 'Incoterms')
-  emitter.emit('clearTag', 'Notify Party')
-  emitter.emit('clearTag', 'Bill to')
-  emitter.emit('clearTag', 'Origin Agent')
-  emitter.emit('clearTag', 'Destination Agent')
-  emitter.emit('clearTag', 'Destination Operator')
-  emitter.emit('clearTag', 'Sales')
-  emitter.emit('clearTag', 'Origin')
-  emitter.emit('clearTag', 'Destination')
-  emitter.emit('clearTag', 'Place of Receipt')
-  emitter.emit('clearTag', 'Port of Loading')
-  emitter.emit('clearTag', 'Place of delivery')
-  emitter.emit('clearTag', 'Port of Discharge')
-  emitter.emit('clearTag', 'Vessel')
-  emitter.emit('clearTag', 'Voyage')
-  searchTableQeuryTracking = {}
-  searchTableQeuryTracking.filterTag = ['All']
-  sessionStorage.setItem('searchTableQeuryTracking', JSON.stringify(searchTableQeuryTracking))
-  if (
-    sessionStorage.getItem('reportList') != null ||
-    sessionStorage.getItem('reportList') != '{}'
-  ) {
-    // sessionStorage.removeItem('reportList')
-    sessionStorage.removeItem('tagsList')
-  }
-  getTrackingData()
-  renderTagsData()
-}
-const renderTagsData = () => {
-  const data = JSON.parse(sessionStorage.getItem('tagsList') as string) || {}
-  if (Object.keys(data).length != 0) {
-    let str = `${data.title}:${data.name}`
-    !filterData.dashboardData.includes(str) && filterData.dashboardData.push(str)
-  }
-  tagsData.value = []
-  if (filterData.transportData.length) {
-    tagsData.value.push(filterData.transportData[0])
-  }
-  if (filterData.daterangeData.length) {
-    filterData.daterangeData.forEach((item) => {
-      tagsData.value.push(item)
-    })
-  }
-  if (filterData.morefiltersData.length) {
-    filterData.morefiltersData.forEach((item) => {
-      tagsData.value.push(item)
-    })
-  }
-  if (filterData.dashboardData.length) {
-    filterData.dashboardData.forEach((item) => {
-      tagsData.value.push(item)
-    })
-  }
-  if (filterData.filtersTagData.length) {
-    filterData.filtersTagData.forEach((item) => {
-      tagsData.value.push(item)
-    })
-  }
-}
-// 清除 Transport Tags
-const clearTransportTags = () => {
-  filterData.transportData = []
-}
-// 清除 Daterange Tags
-const clearDaterangeTags = () => {
-  filterData.daterangeData = []
-}
-// 清除 MoreFilters Tags
-const clearMoreFiltersTags = () => {
-  filterData.morefiltersData = []
-}
-
-const initDataByHeaderSearch = () => {
-  const data = JSON.parse(localStorage.getItem('searchData'))
-  if (data) {
-    // 更新搜索框的值
-    TrackingSearch.value = data.searchValue
-    // 更新表格数据
-    TrackingTable_ref.value.getSharedTableData()
-    // 更新tagsList和TransportList
-    TransportListItem.value = data.trackingData.TransportList
-    TagsList.value = data.trackingData.tagsList
-    let obj = {
-      IncotermsList: data.trackingData.IncotermsList,
-      ServiceList: data.trackingData.ServiceList
-    }
-    sessionStorage.setItem('incotermsList', JSON.stringify(obj))
-    headerSearch.clearSearchData()
-    tagsData.value = []
-    filterData.transportData = []
-    filterData.daterangeData = []
-    filterData.morefiltersData = []
-    filterData.dashboardData = []
-    filterData.filtersTagData = []
-    let str = 'Shipment status: All'
-    filterData.filtersTagData.push(str)
-    searchTableQeuryTracking = {}
-    sessionStorage.removeItem('searchTableQeuryTracking')
-    searchTableQeuryTracking._textSearch = TrackingSearch.value
-    searchTableQeuryTracking.filterTag = ['All']
-
-    sessionStorage.setItem('searchTableQeuryTracking', JSON.stringify(searchTableQeuryTracking))
-    setTimeout(() => {
-      emitter.emit('clearTag', 'Shipment status')
-      emitter.emit('clearTag', 'Transport Mode')
-      emitter.emit('clearTag', 'ETD')
-      emitter.emit('clearTag', 'ETA')
-      emitter.emit('clearTag', 'Creation Time')
-      emitter.emit('clearTag', 'Shippername')
-      emitter.emit('clearTag', 'Consigneename')
-      emitter.emit('clearTag', 'Service')
-      emitter.emit('clearTag', 'Incoterms')
-      emitter.emit('clearTag', 'Notify Party')
-      emitter.emit('clearTag', 'Bill to')
-      emitter.emit('clearTag', 'Origin Agent')
-      emitter.emit('clearTag', 'Destination Agent')
-      emitter.emit('clearTag', 'Destination Operator')
-      emitter.emit('clearTag', 'Sales')
-      emitter.emit('clearTag', 'Origin')
-      emitter.emit('clearTag', 'Destination')
-      emitter.emit('clearTag', 'Place of Receipt')
-      emitter.emit('clearTag', 'Port of Loading')
-      emitter.emit('clearTag', 'Place of delivery')
-      emitter.emit('clearTag', 'Port of Discharge')
-      emitter.emit('clearTag', 'Vessel')
-      emitter.emit('clearTag', 'Voyage')
-    }, 2000)
-    renderTagsData()
-  }
-}
+const filtersStore = useFiltersStore()
+const filtersList = computed(() => filtersStore.filtersList)
 
+const headerSearch = useHeaderSearch()
 // 从 store 中获取数据并绑定到输入框
-const headerSearchdData = computed(() => headerSearch.searchValue)
+const headerSearchValue = computed(() => headerSearch.searchValue)
 // 监听顶部搜索结果的变化
 watch(
-  () => headerSearchdData.value,
+  () => headerSearchValue.value,
   (newData) => {
     if (newData) {
-      initDataByHeaderSearch()
+      // 从顶部获取的值
+      filtersStore.clearFilters()
+      filtersStore.updateFilter({
+        title: 'Shipment Status',
+        value: ['All'],
+        keyType: 'array',
+        key: 'filterTag'
+      })
+      textSearch.value = headerSearchValue.value
+      filtersStore.updateFilter({
+        title: 'text search',
+        value: textSearch.value,
+        keyType: 'normal',
+        key: '_textSearch',
+        isHide: true
+      })
+      getTrackingData()
     }
   }
 )
 
-const TrackingTable_ref = ref()
-const TransportListItem = ref()
-interface ListItem {
-  name: string
-  number: number
-  type: string
-  checked: boolean
-}
-const TagsList = ref<ListItem[]>([])
-const filterTag = ref(['All'])
-const isShowAlertIcon = ref(false)
-const getTrackingData = () => {
-  const dateRangeKeys = [
-    'etd_start',
-    'etd_end',
-    'eta_start',
-    'eta_end',
-    'created_time_start',
-    'created_time_end'
-  ]
-  const curRangeData = {}
-  for (const key of dateRangeKeys) {
-    if (searchTableQeuryTracking[key]) {
-      curRangeData[key] = dayjs(searchTableQeuryTracking[key], formatDate).format(valueFormatDate)
-    }
+const textSearch = ref()
+const tableLoadingTableData = ref(false)
+
+const tabList = ref([
+  {
+    name: 'All',
+    number: 0,
+    type: 'all',
+    checked: true
+  },
+  {
+    name: 'Created',
+    number: 0,
+    type: 'created',
+    checked: false
+  },
+  {
+    name: 'Cargo Received',
+    number: 0,
+    type: 'cargo_received',
+    checked: false
+  },
+  {
+    name: 'Departed',
+    number: 0,
+    type: 'departed',
+    checked: false
+  },
+  {
+    name: 'Arrived',
+    number: 0,
+    type: 'arrived',
+    checked: false
+  },
+  {
+    name: 'Completed',
+    number: 0,
+    type: 'completed',
+    checked: false
   }
+])
+
+const getTrackingData = () => {
+  const queryData = filtersStore.getQueryData
 
   tableLoadingTableData.value = true
-  TrackingTable_ref.value.getLoadingData(tableLoadingTableData.value)
+  trackingTable_ref.value.getLoadingData(tableLoadingTableData.value)
   $api
     .getTrackingTableData({
-      cp: TrackingTable_ref.value.pageInfo.pageNo,
-      ps: TrackingTable_ref.value.pageInfo.pageSize,
+      cp: trackingTable_ref.value.pageInfo.pageNo,
+      ps: trackingTable_ref.value.pageInfo.pageSize,
       rc: -1,
       other_filed: '',
-      ...searchTableQeuryTracking,
-      ...curRangeData
+      _textSearch: textSearch.value,
+      ...queryData
     })
     .then((res: any) => {
       if (res.code === 200) {
-        TransportListItem.value = res.data.TransportList
-        TagsList.value = res.data.tagsList
-        let obj = {
-          IncotermsList: res.data.IncotermsList,
-          ServiceList: res.data.ServiceList
-        }
-        let taglist: any = []
-        if (Object.keys(filterData.filtersTagData).length == 0) {
-          TagsList.value.forEach((item: any) => {
-            if (item.checked == true) {
-              taglist.push(item.name)
-            }
-          })
-          let str = 'Shipment status: ' + taglist.toString()
-          filterData.filtersTagData.push(str)
-          filterData.filtersTagData.forEach((item) => {
-            tagsData.value.push(item)
-          })
-        }
-        sessionStorage.setItem('incotermsList', JSON.stringify(obj))
+        transportListItem.value = res.data.TransportList
+
+        tabList.value = res.data.tagsList
+        const checkedTabNames = tabList.value
+          .filter((item) => item.checked)
+          .map((item) => item.name)
+        filtersStore.updateFilter({
+          title: 'Shipment Status',
+          value: checkedTabNames,
+          keyType: 'array',
+          key: 'filterTag'
+        })
+        incotermsList.value = res.data.IncotermsList
+        serviceList.value = res.data.ServiceList
+
         sessionStorage.setItem('TrackingData', JSON.stringify(res.data))
-        TrackingTable_ref.value.searchTableData(searchTableQeuryTracking)
+        trackingTable_ref.value.searchTableData()
         // 查询没结果的话显示icon
-        if (TrackingSearch.value != '' && TrackingSearch.value != undefined) {
+        if (textSearch.value != '' && textSearch.value != undefined) {
           if (res.data.searchData.length == 0) {
             isShowAlertIcon.value = true
           }
@@ -624,96 +134,132 @@ const getTrackingData = () => {
         }
       }
     })
-    .finally(() => {
-      sessionStorage.removeItem('clickParams')
+}
+onMounted(() => {})
+const initPage = () => {
+  if (headerSearchValue.value) {
+    filtersStore.clearFilters()
+    filtersStore.updateFilter({
+      title: 'Shipment Status',
+      value: ['All'],
+      keyType: 'array',
+      key: 'filterTag'
+    })
+    textSearch.value = headerSearchValue.value
+    filtersStore.updateFilter({
+      title: 'text search',
+      value: textSearch.value,
+      keyType: 'normal',
+      key: '_textSearch',
+      isHide: true
     })
+  } else if (!filtersList.value || (filtersList.value && filtersList.value.length == 0)) {
+    filtersStore.updateFilter({
+      title: 'Transport Mode',
+      value: ['All'],
+      keyType: 'array',
+      key: 'transport_mode'
+    })
+    filtersStore.updateFilter({
+      title: 'ETD',
+      value: [
+        dayjs().subtract(2, 'month').startOf('month').format(formatDate),
+        dayjs().add(1, 'month').format(formatDate)
+      ],
+      keyType: 'dateRange',
+      key: ['etd_start', 'etd_end']
+    })
+    filtersStore.updateFilter({
+      title: 'Shipment Status',
+      value: ['All'],
+      keyType: 'array',
+      key: 'filterTag'
+    })
+  }
+  getTrackingData()
 }
+
 onMounted(() => {
-  if (
-    sessionStorage.getItem('clickParams') != null &&
-    sessionStorage.getItem('clickParams') != '{}'
-  ) {
-    const reportList = JSON.parse(sessionStorage.getItem('reportList') as string) || {}
-    for (const key in reportList) {
-      let str = ''
-      switch (key) {
-        case 'etd_start':
-          const startDate = dayjs(reportList['etd_start'], valueFormatDate).format(formatDate)
-          searchTableQeuryTracking['etd_start'] = startDate
-          const endDate = dayjs(reportList['etd_end'], valueFormatDate).format(formatDate)
-          searchTableQeuryTracking['etd_end'] = endDate
-          const startEnd = startDate + ' To ' + endDate
-          str = `ETD:${startEnd}`
-          // filterData.daterangeData.push(str)
-          break
+  initPage()
+})
 
-        case 'etd_end':
-          break
-        case 'eta_start':
-          const etaStart = dayjs(reportList['eta_start'], valueFormatDate).format(formatDate)
-          searchTableQeuryTracking['eta_start'] = etaStart
-          const etaEnd = dayjs(reportList['eta_end'], valueFormatDate).format(formatDate)
-          searchTableQeuryTracking['eta_end'] = etaEnd
-          str = `ETA:${etaStart} To ${etaEnd}`
-          // filterData.daterangeData.push(str)
-          break
+//defaultDate
 
-        case 'eta_end':
-          break
-        case 'created_time_start':
-          const createdTimeStart = dayjs(reportList['created_time_start'], valueFormatDate).format(
-            formatDate
-          )
-          searchTableQeuryTracking['created_time_start'] = createdTimeStart
-          const createdTimeEnd = dayjs(reportList['created_time_end'], valueFormatDate).format(
-            formatDate
-          )
-          searchTableQeuryTracking['created_time_end'] = createdTimeEnd
-          str = `Creation Time:${createdTimeStart} To ${createdTimeEnd}`
-          // filterData.daterangeData.push(str)
-          break
-        case 'created_time_end':
-          break
-        default:
-          searchTableQeuryTracking[key] = reportList[key]
-      }
-    }
+const trackingTable_ref = ref()
+const transportListItem = ref()
 
-    // sessionStorage.removeItem('reportList')
-    sessionStorage.setItem('searchTableQeuryTracking', JSON.stringify(searchTableQeuryTracking))
-    getTrackingData()
-  }
-  renderTagsData()
-})
-const changeTag = (val: any, boolean: any) => {
-  filterData.filtersTagData = []
-  searchTableQeuryTracking.filterTag = val
-  let str = 'Shipment status: ' + val
-  filterData.filtersTagData.push(str)
-  filterTag.value = val
+const isShowAlertIcon = ref(false)
+const incotermsList = ref([])
+const serviceList = ref([])
 
-  sessionStorage.setItem('searchTableQeuryTracking', JSON.stringify(searchTableQeuryTracking))
-  if (boolean) {
-    getTrackingData()
-  }
-  renderTagsData()
+const tabChange = (changeTabList: any) => {
+  tabList.value = changeTabList
+  const checkedTabNames = tabList.value.filter((item) => item.checked).map((item) => item.name)
+  filtersStore.updateFilter({
+    title: 'Shipment Status',
+    value: checkedTabNames,
+    keyType: 'array',
+    key: 'filterTag'
+  })
+
+  getTrackingData()
 }
+const getTabsList = (list) => {
+  tabList.value.forEach((item) => {
+    const current = list.find((i) => i.name === item.name)
+    if (current) {
+      item.number = current.number
+    }
+  })
+}
+
 // 点击search按钮
 const SearchInput = () => {
-  searchTableQeuryTracking._textSearch = TrackingSearch.value
-
-  sessionStorage.setItem('searchTableQeuryTracking', JSON.stringify(searchTableQeuryTracking))
+  filtersStore.updateFilter({
+    title: 'text search',
+    value: textSearch.value,
+    keyType: 'normal',
+    key: '_textSearch',
+    isHide: true
+  })
   getTrackingData()
 }
 
 import TrackingGuide from './components/TrackingGuide.vue'
 import { useGuideStore } from '@/stores/modules/guide'
+import { onBeforeRouteLeave } from 'vue-router'
 
 const guideStore = useGuideStore()
 const trackingGuideRef = ref()
 const handleGuide = () => {
   trackingGuideRef.value.startGuide() // 开始引导
 }
+
+const clearfilters = () => {
+  filtersStore.clearFilters()
+  getTrackingData()
+}
+
+const handleClose = (tagTitle: any) => {
+  filtersStore.deleteFilterByTitle(tagTitle)
+  getTrackingData()
+}
+
+const clearDaterangeTags = () => {
+  getTrackingData()
+}
+
+const clearMoreFiltersTags = () => {
+  getTrackingData()
+}
+
+onBeforeRouteLeave((route: any) => {
+  // guideStore.clearGuide()
+  const whiteList = ['Booking Detail', 'Tracking Detail', 'Add VGM']
+  if (!whiteList.includes(route?.name)) {
+    filtersStore.clearFilters()
+  }
+})
 </script>
 
 <template>
@@ -725,12 +271,12 @@ const handleGuide = () => {
   <div class="display" ref="filterRef">
     <div class="filter-box">
       <div class="filters-container" id="filters-container-guide">
-        <FilterTags :TagsListItem="TagsList" @changeTag="changeTag"></FilterTags>
+        <FilterTags :tagsList="tabList" @tabChange="tabChange"></FilterTags>
         <div class="heaer_top">
           <div class="search">
             <el-input
               placeholder="Enter Booking/HBL/PO/Container/Carrier Booking No. "
-              v-model="TrackingSearch"
+              v-model="textSearch"
               class="log_input"
               @keyup.enter="SearchInput"
             >
@@ -761,62 +307,57 @@ const handleGuide = () => {
           </div>
 
           <TransportMode
-            :isShipment="true"
-            :TransportListItem="TransportListItem"
-            @TransportSearch="TransportSearch"
-            @defaultTransport="defaultTransport"
-            @clearTransportTags="clearTransportTags"
+            :transportListItem="transportListItem"
+            @TransportSearch="getTrackingData()"
           ></TransportMode>
           <DateRange
-            :isShipment="true"
-            @DateRangeSearch="DateRangeSearch"
+            @DateRangeSearch="getTrackingData()"
             @clearDaterangeTags="clearDaterangeTags"
-            @defaultDate="defaultDate"
           ></DateRange>
         </div>
       </div>
       <MoreFilters
-        :isShipment="true"
-        :searchTableQeury="searchTableQeuryTracking"
-        @MoreFiltersSearch="MoreFiltersSearch"
+        @handleSearch="getTrackingData()"
+        :incotermsList="incotermsList"
+        :serviceList="serviceList"
         @clearMoreFiltersTags="clearMoreFiltersTags"
-        @defaultMorefilters="defaultMorefilters"
         :isShowMoreFiltersGuidePhoto="guideStore.tracking.isShowMoreFiltersGuidePhoto"
-        :pageMode="'tracking'"
       ></MoreFilters>
       <el-button class="el-button--dark" style="margin-left: 8px" @click="SearchInput"
         >Search</el-button
       >
     </div>
     <!-- 筛选项 -->
-    <div class="filtersTag" v-if="tagsData.length" id="filter-tag-guide">
+    <div class="filtersTag" v-if="filtersStore.getTagsList.length" id="filter-tag-guide">
       <el-tag
-        :key="tag"
+        :key="tag.title"
         class="tag"
-        v-for="tag in tagsData"
-        :closable="!tag.includes('Shipment')"
+        v-for="tag in filtersStore.getTagsList"
+        :closable="!tag.title.includes('Shipment')"
         :disable-transitions="false"
-        @close="handleClose(tag)"
+        @close="handleClose(tag.title)"
         color="#EFEFF0"
       >
         <el-tooltip
           class="box-item"
           effect="dark"
-          :content="tag"
+          :content="tag.value"
           placement="top"
-          v-if="tag.length > 39"
+          :popper-style="{ maxWidth: '500px', whiteSpace: 'normal', wordWrap: 'break-word' }"
+          v-if="tag.value.length > 39"
         >
-          {{ tag.length > 39 ? tag.substr(0, 39) + '...' : tag }}
+          {{ tag.value.length > 39 ? tag.value.slice(0, 39) + '...' : tag.value }}
         </el-tooltip>
-        <div v-else>{{ tag }}</div>
+        <div v-else>{{ tag.value }}</div>
       </el-tag>
       <div class="text_button" @click="clearfilters">Clear Filters</div>
     </div>
   </div>
   <TrackingTable
+    :textSearch="textSearch"
     :height="containerHeight"
-    :tagsData="tagsData"
-    ref="TrackingTable_ref"
+    ref="trackingTable_ref"
+    @getTabsList="getTabsList"
   ></TrackingTable>
 </template>
 

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

@@ -94,7 +94,7 @@ const openShareDialog = () => {
           :class="[`icon-${transportationMode?.[allData?.transportInfo?.mode]}`]"
           style="font-size: 64px"
         ></span>
-        <div class="no">Tracking No. {{ allData?.transportInfo['Tracking No.'] }}</div>
+        <div class="no">House No. {{ allData?.transportInfo['Tracking No.'] }}</div>
         <VTag large :type="allData?.transportInfo?.status">{{
           allData?.transportInfo?.status
         }}</VTag>

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

@@ -160,7 +160,7 @@ const SubscribeShipments = () => {
           :class="[`icon-${transportationMode?.[allData?.transportInfo?.mode]}`]"
           style="font-size: 64px"
         ></span>
-        <div class="no">Tracking No. {{ allData?.transportInfo?.['Tracking No.'] }}</div>
+        <div class="no">House No. {{ allData?.transportInfo?.['Tracking No.'] }}</div>
         <VTag large :type="allData?.transportInfo?.status">{{
           allData?.transportInfo?.status
         }}</VTag>

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

@@ -2,7 +2,7 @@
 import '@wangeditor/editor/dist/css/style.css'
 import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
 import { i18nChangeLanguage, DomEditor } from '@wangeditor/editor'
-import { formatTimezone } from '@/utils/tools'
+import { formatTimezoneByUser } from '@/utils/tools'
 
 i18nChangeLanguage('en')
 
@@ -228,7 +228,9 @@ const sendEmail = () => {
             <div>{{ item.name?.slice(0, 1) }}</div>
           </div>
           <div class="name">{{ item.name }}</div>
-          <div class="date">{{ formatTimezone(item.creatTime) }}</div>
+          <div class="date">
+            {{ formatTimezoneByUser(item.creatTime, 'MM/DD/YYYY HH:mm:ss', true) }}
+          </div>
         </div>
         <div class="content">
           {{ item.content }}

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

@@ -122,13 +122,13 @@ 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)
   ) {
     // , DOCX, and XLSX
-    ElMessage.error('The file types allowed for upload are: PDF.')
+    ElMessage.error('The file types allowed for upload are: PDF, XLSX and DOCX.')
     return false
   } else if (rawFile.size / 1024 / 1024 > 5) {
     ElMessage.error('File size must not exceed 5MB!')
@@ -165,7 +165,7 @@ const disableUpload = ref(false)
         ref="uploadRef"
         drag
         :limit="5"
-        :accept="'application/pdf'"
+        :accept="'.pdf,.xlsx,.docx'"
         :show-file-list="false"
         :action="url"
         :auto-upload="false"
@@ -183,7 +183,7 @@ const disableUpload = ref(false)
           <div class="label">
             <span class="font_family icon-icon_info_b" style="vertical-align: baseline"></span>
             <!-- , docx, xlsx  -->
-            <span>Supported formats: pdf ; </span>
+            <span>Supported formats: .pdf, .xlsx, .docx </span>
           </div>
           <span>Maximum Size: 5MB; </span>
           <span>Maximum Number: 5 files</span>

+ 21 - 65
src/views/Tracking/src/components/TrackingTable/src/TrackingTable.vue

@@ -6,11 +6,15 @@ import dayjs from 'dayjs'
 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'
 import { useVisitedRowState } from '@/stores/modules/visitedRow'
 import { formatTimezone, formatNumber } from '@/utils/tools'
 import { useTrackingDownloadData } from '@/stores/modules/trackingDownloadData'
+import { useFiltersStore } from '@/stores/modules/filtersList'
+import { useHeaderSearch } from '@/stores/modules/headerSearch'
+
+const filtersStore = useFiltersStore()
+const headerSearchStore = useHeaderSearch()
 
 const visitedRowState = useVisitedRowState()
 const themeStore = useThemeStore()
@@ -22,9 +26,9 @@ const props = defineProps({
     type: Number,
     default: 440
   },
-  tagsData: {
-    type: Array,
-    default: () => []
+  textSearch: {
+    type: String,
+    default: ''
   }
 })
 const tableLoadingTable = ref()
@@ -154,40 +158,27 @@ const assignTableData = (data: any) => {
   }, 1000)
 }
 
-const filterdataobj = ref()
-const getSharedTableData = () => {
-  const trackingData = JSON.parse(localStorage.getItem('searchData'))?.trackingData
-  if (trackingData) {
-    pageInfo.value.pageSize = Number(trackingData.ps)
-    sessionStorage.setItem('trackingTablePageInfo', JSON.stringify(pageInfo.value))
-    assignTableData(trackingData)
-    selectedNumber.value = 0
-    selectedTableData.value = []
-    nextTick(() => {
-      tableRef.value && autoWidth(trackingTable.value, tableRef.value)
-    })
-    return true
-  }
-  return false
-}
-
+const emit = defineEmits(['getTabsList'])
 // 切换分页时重新获取表格数据
 const getTableData = async (isPageChange?: boolean) => {
   // 保存页长以及当前页码
   sessionStorage.setItem('trackingTablePageInfo', JSON.stringify(pageInfo.value))
   tableLoadingTable.value = true
   const rc = isPageChange ? pageInfo.value.total : -1
+  const queryData = filtersStore.getQueryData
   await $api
     .getTrackingTableData({
       cp: pageInfo.value.pageNo,
       ps: pageInfo.value.pageSize,
       rc,
       other_filed: '',
-      ...filterdataobj.value
+      _textSearch: props.textSearch,
+      ...queryData
     })
     .then((res: any) => {
       if (res.code === 200) {
         assignTableData(res.data)
+        emit('getTabsList', res.data.tagsList)
       }
     })
     .finally(() => {
@@ -198,46 +189,10 @@ const getTableData = async (isPageChange?: boolean) => {
       selectedNumber.value = 0
       selectedTableData.value = []
     })
-  // if (
-  //   sessionStorage.getItem('clickParams') != null &&
-  //   sessionStorage.getItem('clickParams') != '{}'
-  // ) {
-  //   const data = JSON.parse(sessionStorage.getItem('clickParams') as string) || {}
-  //   assignTableData(data)
-  //   nextTick(() => {
-  //     tableRef.value && autoWidth(trackingTable.value, tableRef.value)
-  //     tableLoadingTable.value = false
-  //     selectedNumber.value = 0
-  //     selectedTableData.value = []
-  //   })
-  // } else {
-  //   await $api
-  //     .getTrackingTableData({
-  //       cp: pageInfo.value.pageNo,
-  //       ps: pageInfo.value.pageSize,
-  //       rc,
-  //       other_filed: '',
-  //       ...filterdataobj.value
-  //     })
-  //     .then((res: any) => {
-  //       if (res.code === 200) {
-  //         assignTableData(res.data)
-  //       }
-  //     })
-  //     .finally(() => {
-  //       nextTick(() => {
-  //         tableRef.value && autoWidth(trackingTable.value, tableRef.value)
-  //         tableLoadingTable.value = false
-  //       })
-  //       selectedNumber.value = 0
-  //       selectedTableData.value = []
-  //     })
-  // }
 }
 
 // 查询列表数据
-const searchTableData = (data: any) => {
-  filterdataobj.value = data
+const searchTableData = () => {
   if (sessionStorage.getItem('TrackingData') != null) {
     const data = JSON.parse(sessionStorage.getItem('TrackingData') as string) || {}
     assignTableData(data)
@@ -250,6 +205,8 @@ const searchTableData = (data: any) => {
         sessionStorage.removeItem('TrackingData')
       })
     }, 100)
+  } else {
+    getTableData()
   }
 }
 
@@ -387,7 +344,6 @@ const handleDownload = () => {
     }
   })
   downloadDialogRef.value.openDialog(
-    props.tagsData,
     curSelectedColumns,
     selectedNumber.value || pageInfo.value.total
   )
@@ -443,7 +399,6 @@ const getExportTableData = (status: number) => {
   $api
     .getAllTrackingTableData({
       selected_fields: column,
-      excel_filter_condition: props.tagsData.join(','),
       tmp_search: tempSearch.value
     })
     .then((res: any) => {
@@ -569,8 +524,6 @@ const handleVGM = (row) => {
   })
 }
 
-const loadingState = useLoadingState()
-
 // 表格右键点击事件
 const handleCurrentRow = (params) => {
   tableRef.value?.setCurrentRow(params.row)
@@ -609,9 +562,12 @@ const SubscribeShipments = (row: any) => {
     })
 }
 
+onUnmounted(() => {
+  headerSearchStore.clearSearchData()
+})
+
 defineExpose({
   searchTableData,
-  getSharedTableData,
   getLoadingData,
   pageInfo
 })
@@ -676,7 +632,7 @@ defineExpose({
 
     <vxe-grid
       ref="tableRef"
-      v-vloading="tableLoadingColumn || tableLoadingTable || loadingState.trackingTableLoading"
+      v-vloading="tableLoadingColumn || tableLoadingTable"
       :height="props.height"
       :style="{ border: 'none' }"
       :row-class-name="handleRowClassName"

+ 23 - 5
src/views/Tracking/src/components/TrackingTable/src/components/DownloadDialog.vue

@@ -1,8 +1,11 @@
 <script setup lang="ts">
+import { useFiltersStore } from '@/stores/modules/filtersList'
+
+const filtersStore = useFiltersStore()
+
 const dialogVisible = ref(false)
 
-const openDialog = (tagsData: string[], selectedColumns: string[], slectedDataNumber: number) => {
-  listData.value = tagsData
+const openDialog = (selectedColumns: string[], slectedDataNumber: number) => {
   selectedDataNumber.value = slectedDataNumber
   columns.value = selectedColumns
   dialogVisible.value = true
@@ -13,8 +16,6 @@ const isShowSelectColumn = ref(false)
 const downloadFilter = ref(1)
 const selectedDataNumber = ref(0)
 
-const listData = ref()
-
 const columns = ref()
 
 const emits = defineEmits<{ export: [number] }>()
@@ -44,7 +45,19 @@ defineExpose({
           </div>
         </div>
         <div class="data-filter">
-          <div class="filter-item" v-for="item in listData" :key="item">{{ item }}</div>
+          <div class="filter-item" v-for="item in filtersStore.getTagsList" :key="item.title">
+            <el-tooltip
+              class="box-item"
+              effect="dark"
+              :content="item.value"
+              placement="top"
+              :popper-style="{ maxWidth: '500px', whiteSpace: 'normal', wordWrap: 'break-word' }"
+              v-if="item.value.length > 39"
+            >
+              {{ item.value.length > 39 ? item.value.slice(0, 39) + '...' : item.value }}
+            </el-tooltip>
+            <div v-else>{{ item.value }}</div>
+          </div>
         </div>
         <div class="download-filter">
           <el-radio-group v-model="downloadFilter">
@@ -129,6 +142,11 @@ defineExpose({
     }
 
     .column-number {
+      display: inline-flex;
+      align-items: center;
+      justify-content: center;
+      min-width: 18px;
+      height: 18px;
       padding: 3px 5px;
       background-color: var(--color-theme);
       border-radius: 12px;

+ 2 - 2
tsconfig.app.json

@@ -12,8 +12,8 @@
   ],
   "compilerOptions": {
     "outDir": "./dist/app",
-    "module": "nodenext",
-    "moduleResolution": "nodenext",
+    "module": "ESNext",
+    "moduleResolution": "Bundler",
     "composite": true,
     "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
     "baseUrl": ".",

File diff ditekan karena terlalu besar
+ 72 - 0
vite.config.ts.timestamp-1772161604204-11dfcc4937af7.mjs


Beberapa file tidak ditampilkan karena terlalu banyak file yang berubah dalam diff ini