Ver código fonte

feat: 合并dev分支代码

Jack Zhou 3 semanas atrás
pai
commit
21bfef2ce3
100 arquivos alterados com 7786 adições e 5617 exclusões
  1. 1 1
      .env.development
  2. 2 0
      .gitignore
  3. 2 1
      package.json
  4. BIN
      public/videos/demo-video.mp4
  5. 6 3
      src/App.vue
  6. 3 1
      src/api/index.ts
  7. 47 0
      src/api/module/Delivery.ts
  8. 0 1
      src/api/module/login.ts
  9. 298 0
      src/api/module/report.ts
  10. 15 0
      src/api/module/system.ts
  11. 37 0
      src/api/module/tracking.ts
  12. 4 4
      src/components/AIRobot/src/AIRobot.vue
  13. 50 66
      src/components/AddRules/src/AddRules.vue
  14. 2 6
      src/components/AddRules/src/components/NotiMethods.vue
  15. 2 4
      src/components/AutoComplete/src/AutoComplete.vue
  16. 77 54
      src/components/AutoSelect/src/AutoSelect.vue
  17. 118 140
      src/components/CreateAddRules/src/CreateAddRules.vue
  18. 2 6
      src/components/CreateAddRules/src/components/NotiMethods.vue
  19. 12 10
      src/components/CustomizeColumns/src/CustomizeColumns.vue
  20. 129 313
      src/components/DateRange/src/DateRange.vue
  21. 8 20
      src/components/DateRange/src/components/CalendarDate.vue
  22. 3 0
      src/components/DateRange/src/components/QuickCalendarDate.vue
  23. 172 0
      src/components/DateRange/src/components/VCalendarDate.vue
  24. 51 66
      src/components/FliterTags/src/FilterTags.vue
  25. 151 975
      src/components/MoreFilters/src/MoreFilters.vue
  26. 242 0
      src/components/MoreFilters/src/components/PartiesView.vue
  27. 230 0
      src/components/MoreFilters/src/components/PlacesView.vue
  28. 130 97
      src/components/MoreFilters/src/components/SelectValue.vue
  29. 0 114
      src/components/SelectTable/src/SelectTable copy.vue
  30. 172 156
      src/components/SelectTable/src/SelectTable.vue
  31. 57 153
      src/components/SelectTableSelect/src/SelectTableSelect.vue
  32. 183 4
      src/components/ShipmentStatus/src/ShipmentStatus.vue
  33. 35 0
      src/components/TableImgEmpty/TableImgEmpty.vue
  34. BIN
      src/components/TableImgEmpty/image/empty-dark.png
  35. BIN
      src/components/TableImgEmpty/image/empty-light.png
  36. 0 0
      src/components/TableImgEmpty/index.ts
  37. 65 176
      src/components/TransportMode/src/TransportMode.vue
  38. 44 44
      src/components/VBreadcrumb/src/VBreadcrumb.vue
  39. 1 0
      src/components/VEllipsisTooltip/index.ts
  40. 212 0
      src/components/VEllipsisTooltip/src/VEllipsisTooltip.vue
  41. 29 11
      src/components/VTag/src/VTag.vue
  42. 176 196
      src/components/selectAutoSelect/src/selectAutoSelect.vue
  43. 8 8
      src/hooks/calculatingHeight.ts
  44. 4 0
      src/main.ts
  45. 133 27
      src/router/index.ts
  46. 37 82
      src/stores/modules/breadCrumb.ts
  47. 115 0
      src/stores/modules/filtersList.ts
  48. 0 18
      src/stores/modules/loadingState.ts
  49. 14 0
      src/stores/modules/notificationMessage.ts
  50. 20 0
      src/stores/modules/trackingDownloadData.ts
  51. 35 3
      src/stores/modules/user.ts
  52. 117 18
      src/styles/Antdui.scss
  53. 16 6
      src/styles/elementui.scss
  54. 228 4
      src/styles/icons/iconfont.css
  55. 0 0
      src/styles/icons/iconfont.js
  56. 2 0
      src/styles/icons/iconfont.svg
  57. BIN
      src/styles/icons/iconfont.ttf
  58. BIN
      src/styles/icons/iconfont.woff
  59. BIN
      src/styles/icons/iconfont.woff2
  60. 4 0
      src/styles/theme-g.scss
  61. 86 3
      src/styles/theme.scss
  62. 4 1
      src/styles/vxeTable.scss
  63. 43 19
      src/utils/axios.ts
  64. 42 39
      src/utils/table.ts
  65. 18 5
      src/utils/tools.ts
  66. 1 1
      src/views/AIApiLog/src/components/LogDialog.vue
  67. 1 2
      src/views/AIRobotChat/src/AIRobotChat.vue
  68. 146 365
      src/views/Booking/src/BookingView.vue
  69. 2 0
      src/views/Booking/src/components/BookingDetail/src/BookingDetail.vue
  70. 12 2
      src/views/Booking/src/components/BookingDetail/src/components/BasicInformation.vue
  71. 4 2
      src/views/Booking/src/components/BookingDetail/src/components/EmailView.vue
  72. 21 11
      src/views/Booking/src/components/BookingTable/src/BookingTable.vue
  73. 18 4
      src/views/Booking/src/components/BookingTable/src/components/DownloadDialog.vue
  74. 419 427
      src/views/Dashboard/src/DashboardView.vue
  75. 238 198
      src/views/Dashboard/src/components/BarChart.vue
  76. 196 0
      src/views/Dashboard/src/components/CustomerFilter.vue
  77. 106 92
      src/views/Dashboard/src/components/DashFiters.vue
  78. 80 39
      src/views/Dashboard/src/components/PieChart.vue
  79. 1 2
      src/views/Dashboard/src/components/RecentStatus.vue
  80. 79 23
      src/views/Dashboard/src/components/RevenueChart.vue
  81. 259 195
      src/views/Dashboard/src/components/SellerChart.vue
  82. 2 2
      src/views/Dashboard/src/components/TopMap.vue
  83. BIN
      src/views/Dashboard/src/image/xiazai.png
  84. 78 263
      src/views/DestinationDelivery/src/DestinationDelivery.vue
  85. 263 0
      src/views/DestinationDelivery/src/components/CalendarTagDetailDialog.vue
  86. 669 0
      src/views/DestinationDelivery/src/components/CalendarView.vue
  87. 10 11
      src/views/DestinationDelivery/src/components/ConfiguRations/src/ConfiguRations.vue
  88. 22 17
      src/views/DestinationDelivery/src/components/ConfiguRations/src/components/ConfigurationsTable.vue
  89. 275 227
      src/views/DestinationDelivery/src/components/ConfiguRations/src/components/CreateNewRule.vue
  90. 245 150
      src/views/DestinationDelivery/src/components/ConfiguRations/src/components/RecommendDate.vue
  91. 16 15
      src/views/DestinationDelivery/src/components/ConfiguRations/src/components/SelectValue.vue
  92. 287 164
      src/views/DestinationDelivery/src/components/CreateNewBooking/src/CreateNewbooking.vue
  93. 209 46
      src/views/DestinationDelivery/src/components/CreateNewBooking/src/components/NewbookingTable.vue
  94. 1 1
      src/views/DestinationDelivery/src/components/DeliveryDate.vue
  95. 294 0
      src/views/DestinationDelivery/src/components/ListView.vue
  96. 0 1
      src/views/DestinationDelivery/src/components/ModifyBooking/index.ts
  97. 0 276
      src/views/DestinationDelivery/src/components/ModifyBooking/src/ModifyBooking.vue
  98. 0 152
      src/views/DestinationDelivery/src/components/ModifyBooking/src/components/SelectShipmentsTable.vue
  99. 26 26
      src/views/DestinationDelivery/src/components/TableView/src/TableView.vue
  100. 112 48
      src/views/DestinationDelivery/src/components/TableView/src/components/ShipmentInforTable.vue

+ 1 - 1
.env.development

@@ -1,2 +1,2 @@
-VITE_API_HOST = 'http://192.168.14.224/Customer_Service_Online'
+VITE_API_HOST = 'http://192.168.114.72/Customer_Service_Online'
 VITE_BASE_URL = '/k_new_online/'

+ 2 - 0
.gitignore

@@ -17,6 +17,8 @@ components.d.ts
 package-lock.json
 pnpm-lock.yaml
 auto-imports.d.ts
+*.rar
+*.zip
 
 stats.html
 

+ 2 - 1
package.json

@@ -40,6 +40,7 @@
     "moment": "^2.30.1",
     "moment-timezone": "^0.5.46",
     "pinia": "^2.2.2",
+    "pinia-plugin-persistedstate": "^4.5.0",
     "sass-loader": "^14.1.1",
     "vue": "^3.4.29",
     "vue-draggable-plus": "^0.5.3",
@@ -72,7 +73,7 @@
     "postcss": "^8.4.41",
     "postcss-loader": "^8.1.1",
     "prettier": "^3.2.5",
-    "rollup-plugin-visualizer": "^6.0.3",
+    "rollup-plugin-visualizer": "^6.0.4",
     "sass": "^1.79.4",
     "typescript": "~5.4.0",
     "unplugin-auto-import": "^0.18.2",

BIN
public/videos/demo-video.mp4


+ 6 - 3
src/App.vue

@@ -2,6 +2,10 @@
 import { RouterView } from 'vue-router'
 import { useLangStore } from '@/stores/modules/lang'
 import { useI18n } from 'vue-i18n'
+import { useRoute } from 'vue-router'
+import VideoView from '@/views/Video/src/VideoView.vue'
+
+const route = useRoute()
 
 const langStore = useLangStore()
 
@@ -41,8 +45,7 @@ onMounted(async () => {
 
 <template>
   <el-config-provider :locale="langStore.ElementLocale">
-    <RouterView />
+    <VideoView v-if="route.name === 'Demo Video'" />
+    <RouterView v-else />
   </el-config-provider>
 </template>
-
-<style scoped></style>

+ 3 - 1
src/api/index.ts

@@ -7,6 +7,7 @@ import * as notificationMessage from './module/notificationMessage'
 import * as system from './module/system'
 import * as AIRobot from './module/AIRobot'
 import * as Delivery from './module/Delivery'
+import * as report from './module/report'
 /**
  * api 对象接口定义
  */
@@ -27,7 +28,8 @@ const apis = generateApiMap({
   ...notificationMessage,
   ...system,
   ...AIRobot,
-  ...Delivery
+  ...Delivery,
+  ...report
 })
 export default {
   ...apis // 取出所有可遍历属性赋值在新的对象上

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

@@ -318,4 +318,51 @@ export const getEmailRecords = (params: any, config: any) => {
     },
     config
   )
+}
+
+/**
+ * create new booking 表格中 Packing List 和 Commercial Invoice 列的下载功能
+ */
+export const downloadBookingTableFile = (params: any, config: any) => {
+  return HttpAxios.post(
+    `${baseUrl}`,
+    {
+      action: 'destination_delivery_booking',
+      operate: 'batch_download_ci_p_file',
+      ...params
+    },
+    config
+  )
+}
+
+
+/**
+ * 获取delivery calendar view日历数据
+ */
+export const getDeliveryCalendarData = (params: any, config: any) => {
+  return HttpAxios.post(
+    `${baseUrl}`,
+    {
+      action: 'destination_delivery_booking',
+      operate: 'search_calendar',
+      ...params
+    },
+    config
+  )
+}
+
+
+/**
+ * 获取delivery calendar 日历的筛选项Consignee
+ */
+export const getDeliveryCalendarConsignee = (params: any, config: any) => {
+  return HttpAxios.post(
+    `${baseUrl}`,
+    {
+      action: 'destination_delivery_load',
+      operate: 'destination_delivery_calendar_consignee',
+      ...params
+    },
+    config
+  )
 }

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

@@ -106,7 +106,6 @@ export const resetAndActivatePassword = (params: any, config: any) => {
   )
 }
 
-
 /**
  * 获取public tracking detail详情数据
  */

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

@@ -0,0 +1,298 @@
+import HttpAxios from '@/utils/axios'
+
+const base = import.meta.env.VITE_API_HOST
+const baseUrl = `${base}/main_new_version.php`
+
+/**
+ * 获取report template management页面筛选项Party ID下拉框数据
+ */
+export const getFilterPartyID = (params: any, config: any) => {
+  return HttpAxios.post(
+    `${baseUrl}`,
+    {
+      action: 'report_config',
+      operate: 'parity_id',
+      ...params
+    },
+    config
+  )
+}
+
+
+/**
+ * 获取report template management表格列数据
+ */
+export const getReportTemplateManagementTable = (params: any, config: any) => {
+  return HttpAxios.post(
+    `${baseUrl}`,
+    {
+      action: 'report_config',
+      operate: 'search',
+      ...params
+    },
+    config
+  )
+}
+
+/**
+ * 更改Report Template Management表格 Is Active状态
+ */
+export const changeReportTemplateIsActive = (params: any, config: any) => {
+  return HttpAxios.post(
+    `${baseUrl}`,
+    {
+      action: 'report_config',
+      operate: 'active',
+      ...params
+    },
+    config
+  )
+}
+
+/**
+ * 获取Report Fields Configuration 列表数据
+ */
+export const getReportFieldsConfiguration = (params: any, config: any) => {
+  return HttpAxios.get(
+    `${baseUrl}`,
+    {
+      action: 'report_config',
+      operate: 'report_field_load',
+      ...params
+    },
+    config
+  )
+}
+
+/**
+ * Create New Report Template页面获取Specific Roles的Party ID数据
+ */
+export const getSpecificRolesPartyID = (params: any, config: any) => {
+  return HttpAxios.post(
+    `${baseUrl}`,
+    {
+      action: 'ajax',
+      operate: 'autody_extend',
+      type: 'apex',
+      ...params
+    },
+    config
+  )
+}
+
+
+/**
+ * Create New Report Template页面获取Specific Roles的Group Name数据
+ */
+export const getSpecificRolesGroupName = (params: any, config: any) => {
+  return HttpAxios.post(
+    `${baseUrl}`,
+    {
+      action: 'ajax',
+      operate: 'autody_extend',
+      type: 'contact_group',
+      ...params
+    },
+    config
+  )
+}
+
+
+/**
+ * Create New Report Template页面获取Specific Roles的account 数据
+ */
+export const getSpecificRolesAccount = (params: any, config: any) => {
+  return HttpAxios.post(
+    `${baseUrl}`,
+    {
+      action: 'ajax',
+      operate: 'autody_extend',
+      type: 'system_account',
+      ...params
+    },
+    config
+  )
+}
+
+
+/**
+ * 保存 Create New Report Template页面数据
+ */
+export const saveNewReportTemplate = (params: any, config: any) => {
+  return HttpAxios.post(
+    `${baseUrl}`,
+    {
+      action: 'report_config',
+      operate: 'save',
+      ...params
+    },
+    config
+  )
+}
+
+/**
+ * 编辑report template
+ */
+export const editReportTemplate = (params: any, config: any) => {
+  return HttpAxios.post(
+    `${baseUrl}`,
+    {
+      action: 'report_config',
+      operate: 'add',
+      ...params
+    },
+    config
+  )
+}
+
+
+/**
+ * 删除report template
+ */
+export const deleteReportTemplate = (params: any, config: any) => {
+  return HttpAxios.post(
+    `${baseUrl}`,
+    {
+      action: 'report_config',
+      operate: 'delete',
+      ...params
+    },
+    config
+  )
+}
+
+/**
+ * 获取Report页表格数据
+ */
+export const getReportPageTable = (params: any, config: any) => {
+  return HttpAxios.post(
+    `${baseUrl}`,
+    {
+      action: 'shipment_status_report',
+      operate: 'report_search',
+      ...params
+    },
+    config
+  )
+}
+
+/**
+ * 获取Report detail页表格数据
+ */
+export const getReportDetailTable = (params: any, config: any) => {
+  return HttpAxios.post(
+    `${baseUrl}`,
+    {
+      action: 'shipment_status_report',
+      operate: 'report_detail',
+      ...params
+    },
+    config
+  )
+}
+
+
+/**
+ * 获取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页表格数据
+ */
+export const getReportAllTableData = (params: any, config: any) => {
+  return HttpAxios.post(
+    `${baseUrl}`,
+    {
+      action: 'shipment_status_report',
+      operate: 'excel',
+      ...params
+    },
+    config
+  )
+}
+
+/**
+ * 获取 Manage Fields 列表数据
+ */
+export const getManageFieldsList = (params: any, config: any) => {
+  return HttpAxios.post(
+    `${baseUrl}`,
+    {
+      action: 'shipment_status_report',
+      operate: 'manage_fileds',
+      ...params
+    },
+    config
+  )
+}
+
+/**
+ * 保存 Manage Fields 列表数据
+ */
+export const saveManageFieldsList = (params: any, config: any) => {
+  return HttpAxios.post(
+    `${baseUrl}`,
+    {
+      action: 'shipment_status_report',
+      operate: 'manage_fileds_save',
+      ...params
+    },
+    config
+  )
+}
+
+/**
+ * 获取 report schedule 页面数据
+ */
+export const getReportScheduleData = (params: any, config: any) => {
+  return HttpAxios.post(
+    `${baseUrl}`,
+    {
+      action: 'shipment_status_report',
+      operate: 'report_schedule',
+      ...params
+    },
+    config
+  )
+}
+
+/**
+ * 保存 report schedule 页面数据
+ */
+export const saveReportScheduleData = (params: any, config: any) => {
+  return HttpAxios.post(
+    `${baseUrl}`,
+    {
+      action: 'shipment_status_report',
+      operate: 'report_schedule_save',
+      ...params
+    },
+    config
+  )
+}
+
+/**
+ * 获取 report schedule 表格数据
+ */
+export const getReportScheduleTable = (params: any, config: any) => {
+  return HttpAxios.post(
+    `${baseUrl}`,
+    {
+      action: 'shipment_status_report',
+      operate: 'report_schedule_search',
+      ...params
+    },
+    config
+  )
+}

+ 15 - 0
src/api/module/system.ts

@@ -157,4 +157,19 @@ export const FirstInitSubscribe = (params: any, config: any) => {
     },
     config
   )
+}
+
+
+/**
+ * 选择Customer之后重新获取登录信息
+ */
+export const handleCustomerSelection = (params: any, config: any) => {
+  return HttpAxios.post(
+    `${baseUrl}`,
+    {
+      action: 'kam_customer_auto_login',
+      ...params
+    },
+    config
+  )
 }

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

@@ -1,4 +1,5 @@
 import HttpAxios from '@/utils/axios'
+import axios from 'axios'
 
 const base = import.meta.env.VITE_API_HOST
 const baseUrl = `${base}/main_new_version.php`
@@ -197,3 +198,39 @@ export const uploadFile = (params: any, config: any) => {
     config
   )
 }
+
+
+/**
+ * 获取Download Attachment页数据
+ */
+export const getDownloadAttachmentData = (params: any, config: any) => {
+  return HttpAxios.post(
+    `${baseUrl}`,
+    {
+      action: 'ocean_order',
+      operate: 'batch_download_load',
+      ...params
+    },
+    config
+  )
+}
+
+/**
+ * 下载对应的附件
+ */
+export const downloadAttachment = (params: any, config: any) => {
+  return axios.post(
+    baseUrl,
+    {
+      action: 'ocean_order',
+      operate: 'batch_download',
+      ...params
+    },
+    {
+      responseType: 'blob',
+      headers: {
+        'Content-Type': 'multipart/form-data'
+      }
+    }
+  )
+}

+ 4 - 4
src/components/AIRobot/src/AIRobot.vue

@@ -34,10 +34,10 @@ const AvatarMouseLeave = () => {
     AIRobotHoverVisible.value = false
   }
 }
-const emit = defineEmits(['AvatarClick', 'handelClickAIDefault','handelclickaiinit'])
+const emit = defineEmits(['AvatarClick', 'handelClickAIDefault', 'handelclickaiinit'])
 // 点击AIRobot图标
 const AvatarClick = () => {
-  if(clicked.value == false) {
+  if (clicked.value == false) {
     emit('handelclickaiinit')
   }
   clicked.value = true
@@ -126,7 +126,7 @@ const handelClick = (item: any) => {
 }
 
 onMounted(() => {
-  if(localStorage.getItem('userInfo') != null) {
+  if (localStorage.getItem('userInfo') != null) {
     AIIconVisible.value = true
   }
   emitter.on('login-success', isShowLogin)
@@ -158,7 +158,7 @@ defineExpose({
       <div class="dialogue_title">Hi! I'm your Freight Assistant, always on call</div>
     </div>
     <div class="flex_end">
-      <div class="dialogue_content"  style="box-shadow: -10px 10px 24px rgba(58, 0, 78, 0.15);">
+      <div class="dialogue_content" style="box-shadow: -10px 10px 24px rgba(58, 0, 78, 0.15)">
         <div class="dialogue_content_title">
           <div class="dialogue_title_left">
             <img src="../image/icon_faq_b@2x.png" width="24px" />

+ 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>

+ 2 - 6
src/components/AddRules/src/components/NotiMethods.vue

@@ -77,11 +77,7 @@ defineExpose({
   <div style="margin-top: 11px">
     <div class="Method">
       <el-checkbox-group v-model="checkMethodList" @change="changeMethod">
-        <el-checkbox
-          class="methodcheckbox"
-          value="By Email"
-          v-if="user_type != null && user_type != 'customer'"
-        >
+        <el-checkbox class="methodcheckbox" value="By Email">
           <div>By Email</div>
           <div class="methos_image"><img src="../images/illustration_email@2x.png" /></div>
         </el-checkbox>
@@ -109,4 +105,4 @@ defineExpose({
 .methos_image {
   margin: 9px 0;
 }
-</style>
+</style>

+ 2 - 4
src/components/AutoComplete/src/AutoComplete.vue

@@ -133,9 +133,7 @@ const handleSearch = _.debounce(() => {
   }, 800)
 }, 500)
 // 分页 请求接口
-const handleCurrentChange = (val: number) => {
-  console.log(`current page: ${val}`)
-}
+const handleCurrentChange = (val: number) => {}
 </script>
 <template>
   <div>
@@ -233,4 +231,4 @@ const handleCurrentChange = (val: number) => {
 :deep(.el-popper) {
   border-radius: 12px !important;
 }
-</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>

+ 118 - 140
src/components/CreateAddRules/src/CreateAddRules.vue

@@ -159,7 +159,8 @@ const Initdata = () => {
             FrequencyDataCon.value = res.data.Container_Status_Update
             MethodsDataCon.value = res.data.Container_Status_Update
             ShipmentRangeCon.value = res.data.Container_Status_Update
-            createObj.Transportstr = res.data.Container_Status_Update.shipment_details.split(';\r\n')[0]
+            createObj.Transportstr =
+              res.data.Container_Status_Update.shipment_details.split(';\r\n')[0]
             createObj.Timestr = res.data.Container_Status_Update.shipment_details.split(';\r\n')[1]
             createListContainer.value.push(createObj.Transportstr)
             createListContainer.value.push(createObj.Timestr)
@@ -179,8 +180,10 @@ const Initdata = () => {
             FrequencyDataDep.value = res.data['Departure/Arrival_Delay']
             MethodsDataDep.value = res.data['Departure/Arrival_Delay']
             ShipmentRangeDep.value = res.data['Departure/Arrival_Delay']
-            createObj.Transportstr = res.data['Departure/Arrival_Delay'].shipment_details.split(';\r\n')[0]
-            createObj.Timestr = res.data['Departure/Arrival_Delay'].shipment_details.split(';\r\n')[1]
+            createObj.Transportstr =
+              res.data['Departure/Arrival_Delay'].shipment_details.split(';\r\n')[0]
+            createObj.Timestr =
+              res.data['Departure/Arrival_Delay'].shipment_details.split(';\r\n')[1]
             createListDeparture.value.push(createObj.Transportstr)
             createListDeparture.value.push(createObj.Timestr)
           } else if (editTablerules_type == 'ETD/ETA_Change') {
@@ -281,7 +284,7 @@ const changecheckCreateRulesMilestone = (val: any, value: any) => {
   savesubscribeobj.shipment_transport_mode = value
   savesubscribeobj.shipment_details = createListMilestone.value.join(';\n')
 }
-const ChangeCheckTimeRulesMilestone = (val: any, time: any, timeend :any) => {
+const ChangeCheckTimeRulesMilestone = (val: any, time: any, timeend: any) => {
   createListMilestone.value = []
   createObj.Timestr = val
   if (val.includes('ETD')) {
@@ -316,7 +319,7 @@ const changecheckCreateRulesContainer = (val: any, value: any) => {
   savesubscribeobj.shipment_transport_mode = value
   savesubscribeobj.shipment_details = createListContainer.value.join(';\n')
 }
-const ChangeCheckTimeRulesContainer = (val: any, time: any, timeend :any) => {
+const ChangeCheckTimeRulesContainer = (val: any, time: any, timeend: any) => {
   createListContainer.value = []
   createObj.Timestr = val
   if (val.includes('ETD')) {
@@ -351,7 +354,7 @@ const changecheckCreateRulesDeparture = (val: any, value: any) => {
   savesubscribeobj.shipment_transport_mode = value
   savesubscribeobj.shipment_details = createListDeparture.value.join(';\n')
 }
-const ChangeCheckTimeRulesDeparture = (val: any, time: any, timeend :any) => {
+const ChangeCheckTimeRulesDeparture = (val: any, time: any, timeend: any) => {
   createListDeparture.value = []
   createObj.Timestr = val
   if (val.includes('ETD')) {
@@ -386,7 +389,7 @@ const changecheckCreateRulesETDChange = (val: any, value: any) => {
   savesubscribeobj.shipment_transport_mode = value
   savesubscribeobj.shipment_details = createListETDChange.value.join(';\n')
 }
-const ChangeCheckTimeRulesETDChange = (val: any, time: any, timeend :any) => {
+const ChangeCheckTimeRulesETDChange = (val: any, time: any, timeend: any) => {
   createListETDChange.value = []
   createObj.Timestr = val
   if (val.includes('ETD')) {
@@ -578,20 +581,19 @@ const SaveSuceessful = () => {
     })
     .then((res: any) => {
       if (res.code === 200) {
-        if(res.data.msg == 'Update Successful') {
+        if (res.data.msg == 'Update Successful') {
           SaveedVisible.value = true
           setTimeout(() => {
             SaveedVisible.value = false
-            sessionStorage.setItem('activeTab', 'Monitoring Settings')
+            sessionStorage.setItem('activeTab', 'System Settings')
             router.push({
-              path: '/SystemSettings',
+              path: '/system-settings',
               query: {}
             })
           }, 3000)
-        } else if(res.data.msg == 'Similar Rule Detected') {
+        } else if (res.data.msg == 'Similar Rule Detected') {
           SaveVisibleDetected.value = true
-        } else if(res.data.msg == 'Unable to Save')
-        SaveVisibleError.value = true
+        } else if (res.data.msg == 'Unable to Save') SaveVisibleError.value = true
       }
     })
 }
@@ -606,15 +608,15 @@ const HandelSaveVisibleDetected = () => {
     })
     .then((res: any) => {
       if (res.code === 200) {
-          SaveedVisible.value = true
-          setTimeout(() => {
-            SaveedVisible.value = false
-            sessionStorage.setItem('activeTab', 'Monitoring Settings')
-            router.push({
-              path: '/SystemSettings',
-              query: {}
-            })
-          }, 3000)
+        SaveedVisible.value = true
+        setTimeout(() => {
+          SaveedVisible.value = false
+          sessionStorage.setItem('activeTab', 'System Settings')
+          router.push({
+            path: '/system-settings',
+            query: {}
+          })
+        }, 3000)
       }
     })
 }
@@ -624,8 +626,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.length == 0 ||
       createObj.Transportstr == '' ||
@@ -637,7 +638,7 @@ const Savesubscribe = () => {
       if (createObj.Timestr == '') {
         missingmessage.value += 'Time, '
       }
-      if (OceanCheckList.value.length == 0 && AirCheckList.value.length == 0 ) {
+      if (OceanCheckList.value.length == 0 && AirCheckList.value.length == 0) {
         missingmessage.value += 'Select Milestone, '
       }
       if (MilFrequencyList.value.length == 0) {
@@ -651,21 +652,17 @@ const Savesubscribe = () => {
     } else {
       savesubscribeobj.ocean_milestone = OceanCheckListCode.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()
@@ -704,9 +701,8 @@ const Savesubscribe = () => {
     }
   } else if (props.TitleType == 'Departure') {
     savesubscribeobj.rules_type = 'Departure/Arrival_Delay'
-    if ( 
-      DelayedDeparturedList.value.length == 0 &&
-      DelayedAirdList.value.length == 0 ||
+    if (
+      (DelayedDeparturedList.value.length == 0 && DelayedAirdList.value.length == 0) ||
       DepFrequencyList.value.length == 0 ||
       DepMethodsList.value == undefined ||
       DepMethodsList.value.length == 0 ||
@@ -731,21 +727,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()
@@ -753,8 +745,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 ||
@@ -779,21 +770,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()
@@ -836,14 +819,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)
 }
 
@@ -1257,80 +1240,64 @@ 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>
   <!-- 保存失败 -->
   <el-dialog v-model="SaveVisibleError" width="480">
-      <div>Duplicate Rule Error.</div>
-      <div>This rule exactly matches an existing rule.</div>
-      <template #footer>
-        <div class="dialog-footer">
-          <el-button
-            class="el-button--danger"
-            @click="SaveVisibleError = false"
-            style="width: 100px"
-          >
-            OK
-          </el-button>
-        </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>
-          Unable to Save
-        </div>
-      </template>
-    </el-dialog>
-    <!-- 三项重合提示 -->
+    <div>Duplicate Rule Error.</div>
+    <div>This rule exactly matches an existing rule.</div>
+    <template #footer>
+      <div class="dialog-footer">
+        <el-button class="el-button--danger" @click="SaveVisibleError = false" style="width: 100px">
+          OK
+        </el-button>
+      </div>
+    </template>
+    <template #header>
+      <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="SaveVisibleDetected" width="480">
-      <div>A similar configuration rule already exists.</div>
-      <div>Would you like to proceed with creating this rule?</div>
-      <template #footer>
-        <div class="dialog-footer">
-          <el-button
-            class="el-button--default"
-            @click="SaveVisibleDetected = false"
-            style="width: 100px"
-          >
-            Cancel
-          </el-button>
-          <el-button
-            class="el-button--warning"
-            @click="HandelSaveVisibleDetected"
-            style="width: 100px"
-          >
-            Save
-          </el-button>
-        </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>
-          Similar Rule Detected
-        </div>
-      </template>
-    </el-dialog>
+    <div>A similar configuration rule already exists.</div>
+    <div>Would you like to proceed with creating this rule?</div>
+    <template #footer>
+      <div class="dialog-footer">
+        <el-button
+          class="el-button--default"
+          @click="SaveVisibleDetected = false"
+          style="width: 100px"
+        >
+          Cancel
+        </el-button>
+        <el-button
+          class="el-button--warning"
+          @click="HandelSaveVisibleDetected"
+          style="width: 100px"
+        >
+          Save
+        </el-button>
+      </div>
+    </template>
+    <template #header>
+      <div class="warning-header dialog-header">
+        <span class="font_family icon-icon_warning_fill_b"></span>
+        Similar Rule Detected
+      </div>
+    </template>
+  </el-dialog>
 </template>
 
 <style lang="scss" scoped>
@@ -1436,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;
@@ -1462,4 +1440,4 @@ defineExpose({
 :deep(.el-collapse) {
   margin-right: 8px;
 }
-</style>
+</style>

+ 2 - 6
src/components/CreateAddRules/src/components/NotiMethods.vue

@@ -65,11 +65,7 @@ const user_type = localStorage.getItem('user_type')
   <div style="margin-top: 11px">
     <div class="Method">
       <el-checkbox-group v-model="checkMethodList" @change="changeMethod">
-        <el-checkbox
-          class="methodcheckbox"
-          value="By Email"
-          v-if="user_type != null && user_type != 'customer'"
-        >
+        <el-checkbox class="methodcheckbox" value="By Email">
           <div>By Email</div>
           <div class="methos_image"><img src="../images/illustration_email@2x.png" /></div>
         </el-checkbox>
@@ -97,4 +93,4 @@ const user_type = localStorage.getItem('user_type')
 .methos_image {
   margin: 9px 0;
 }
-</style>
+</style>

+ 12 - 10
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)
 }
@@ -166,10 +169,12 @@ const getData = async (reset?: string) => {
 }
 
 const params = ref()
+const tipsString = ref('')
 // rightDistance是右侧箭头消失所需要translateX的值
-const openDialog = async (paramsData: Object, rightDistance: number) => {
+const openDialog = async (paramsData: Object, rightDistance: number, tips: string) => {
   firstLoad.value = localStorage.getItem('firstLoadCustomizeColumns')
   params.value = paramsData
+  tipsString.value = tips
   dialogVisible.value = true
   await getData()
   rightArrowHideDistance.value = rightDistance
@@ -233,23 +238,23 @@ const handleRightRemove = (e: any) => {
     return index !== -1
   })
 
-  if (curGroup.name !== originalGroup.name && curGroup.name !== 'All') {
+  if (curGroup.name !== originalGroup?.name && curGroup.name !== 'All') {
     // 从当前分组中删除移入的数据
     curGroup.children.forEach((item: any, index: number) => {
       item.field === curItem.field && curGroup.children.splice(index, 1)
     })
     // 在对应分组中添加移入的数据
     groupColumns.value.forEach((item: any) => {
-      item.name === originalGroup.name && item.children.push(curItem)
+      item.name === originalGroup?.name && item.children.push(curItem)
     })
     // 添加到All分组里
     groupColumns.value[0].children.push(curItem)
   } else if (curGroup.name === 'All') {
     // 在对应分组中添加移入的数据
     groupColumns.value.forEach((item: any) => {
-      item.name === originalGroup.name && item.children.push(curItem)
+      item.name === originalGroup?.name && item.children.push(curItem)
     })
-  } else if (curGroup.name === originalGroup.name) {
+  } else if (curGroup.name === originalGroup?.name) {
     groupColumns.value[0].children.push(curItem)
   }
 }
@@ -272,7 +277,7 @@ const handleDeleteSelect = (curItem: any) => {
   })
   // 在对应分组中添加移入的数据
   groupColumns.value.forEach((item: any) => {
-    item.name === originalGroup.name && item.children.push(curItem)
+    item.name === originalGroup?.name && item.children.push(curItem)
   })
   // 添加到All分组里
   groupColumns.value[0].children.push(curItem)
@@ -361,10 +366,7 @@ defineExpose({
       </div>
       <div class="tips">
         <span style="font-size: 16px">* </span>
-        <span
-          >Drag item over to this selection or click "add" icon to show the column on your
-          {{ route.path.includes('booking') ? 'booking' : 'shipment' }} list</span
-        >
+        <span>{{ tipsString }}</span>
       </div>
     </div>
     <div class="draggable-list">

+ 129 - 313
src/components/DateRange/src/DateRange.vue

@@ -1,319 +1,144 @@
 <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(() => {
-  defaultDate()
-  emitter.on('clearTag', (tag: any) => {
-    if (tag.includes('ETD')) {
-      clearDateStart()
-    }
-    if (tag.includes('ETA')) {
-      clearDateEnd()
-    }
-    if (tag.includes('Creation Time')) {
-      clearDateEnd()
-    }
-  })
-  emitter.on('clearDaterangeObj', () => {
-    clearDaterangeObj()
-  })
-})
+const filtersStore = useFiltersStore()
+const filtersList = computed(() => filtersStore.filtersList)
 
-onBeforeMount(() => {
-  emitter.off('clearTag')
-  emitter.off('clearDaterangeObj')
-})
-
-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({
-    value: '',
-    number: ''
+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: ['', '']
   })
 }
 const deleteType = (i: any) => {
-  AddDateType.value.splice(i, 1)
-  DateType.value = ''
-  clearDateCreation()
-}
-const props = defineProps({
-  isShipment: Boolean
-})
-let daterangeObj: any = {}
-const DateStart = ref()
-DateStart.value = []
-const DateEnd = ref()
-DateEnd.value = []
-const DateCreation = ref()
-DateCreation.value = []
-const searchTableQeury = ref()
-const searchTableQeuryTracking = ref()
-// 查询默认日期
-const defaultDate = () => {
-  if (props.isShipment) {
-    if (
-      sessionStorage.getItem('clickParams') == null ||
-      sessionStorage.getItem('clickParams') == '{}'
-    ) {
-      if (sessionStorage.getItem('searchTableQeuryTracking') == null) {
-        DateStart.value = [dayjs().subtract(2, 'month').startOf('month'), dayjs().add(1, 'month')]
-        daterangeObj.ETD =
-          DateStart.value[0].format(formatDate) + ' To ' + DateStart.value[1].format(formatDate)
-        const obj = {
-          title: 'ETD',
-          data: [
-            DateStart.value[0].format(valueFormatDate),
-            DateStart.value[1].format(valueFormatDate)
-          ]
-        }
+  otherDateType.value.splice(i, 1)
+}
 
-        daterangeObj2.ETD = obj
-      } else {
-        searchTableQeuryTracking.value =
-          JSON.parse(sessionStorage.getItem('searchTableQeuryTracking') as string) || {}
-        if (searchTableQeuryTracking.value.etd_start) {
-          DateStart.value = [
-            dayjs(searchTableQeuryTracking.value.etd_start),
-            dayjs(searchTableQeuryTracking.value.etd_end)
-          ]
-          daterangeObj.ETD =
-            DateStart.value[0].format(formatDate) + ' To ' + DateStart.value[1].format(formatDate)
-          const obj = {
-            title: 'ETD',
-            data: [
-              DateStart.value[0].format(valueFormatDate),
-              DateStart.value[1].format(valueFormatDate)
-            ]
-          }
-          daterangeObj2.ETD = obj
-        }
-        if (searchTableQeuryTracking.value.eta_start) {
-          DateEnd.value = [
-            dayjs(searchTableQeuryTracking.value.eta_start),
-            dayjs(searchTableQeuryTracking.value.eta_end)
-          ]
-          daterangeObj.ETA =
-            DateEnd.value[0].format(formatDate) + ' To ' + DateEnd.value[1].format(formatDate)
-          const obj = {
-            title: 'ETA',
-            data: [
-              DateEnd.value[0].format(valueFormatDate),
-              DateEnd.value[1].format(valueFormatDate)
-            ]
-          }
-          daterangeObj2.ETA = obj
-        }
-        if (searchTableQeuryTracking.value.created_time_start) {
-          DateCreation.value = [
-            dayjs(searchTableQeuryTracking.value.created_time_start),
-            dayjs(searchTableQeuryTracking.value.created_time_end)
-          ]
-          daterangeObj['Creation Time'] =
-            DateCreation.value[0].format(formatDate) +
-            ' To ' +
-            DateCreation.value[1].format(formatDate)
-          const obj = {
-            title: 'Creation Time',
-            data: [
-              DateCreation.value[0].format(valueFormatDate),
-              DateCreation.value[1].format(valueFormatDate)
-            ]
-          }
-          daterangeObj2['Creation Time'] = obj
-        }
-      }
-      emit('defaultDate', daterangeObj, daterangeObj2, searchTableQeuryTracking.value)
-    } else {
-      const data = JSON.parse(sessionStorage.getItem('reportList') as string) || {}
-      searchTableQeuryTracking.value =
-        JSON.parse(sessionStorage.getItem('searchTableQeuryTracking') as string) || {}
-      if (data.eta_start) {
-        DateEnd.value = [dayjs(data.eta_start), dayjs(data.eta_end)]
-        daterangeObj.ETA =
-          DateEnd.value[0].format(formatDate) + ' To ' + DateEnd.value[1].format(formatDate)
-        const obj = {
-          title: 'ETA',
-          data: [DateEnd.value[0].format(valueFormatDate), DateEnd.value[1].format(valueFormatDate)]
-        }
+const etdDateRange = ref([])
+const etaDateRange = ref([])
 
-        daterangeObj2.ETA = obj
-        emit('defaultDate', daterangeObj, daterangeObj2, searchTableQeuryTracking.value)
-      }
-      if (data.etd_start) {
-        DateStart.value = [dayjs(data.etd_start), dayjs(data.etd_end)]
-        daterangeObj.ETD =
-          DateStart.value[0].format(formatDate) + ' To ' + DateStart.value[1].format(formatDate)
-        const obj = {
-          title: 'ETD',
-          data: [
-            DateStart.value[0].format(valueFormatDate),
-            DateStart.value[1].format(valueFormatDate)
-          ]
-        }
-        daterangeObj2.ETD = obj
-        emit('defaultDate', daterangeObj, daterangeObj2, searchTableQeuryTracking.value)
-      }
-    }
+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 {
-    if (sessionStorage.getItem('searchTableQeury') == null) {
-      DateStart.value = [dayjs().subtract(2, 'month').startOf('month'), dayjs().add(1, 'month')]
-      daterangeObj.ETD =
-        DateStart.value[0].format(formatDate) + ' To ' + DateStart.value[1].format(formatDate)
-      const obj = {
-        title: 'ETD',
-        data: [
-          DateStart.value[0].format(valueFormatDate),
-          DateStart.value[1].format(valueFormatDate)
-        ]
-      }
-      daterangeObj2.ETD = obj
-    } else {
-      searchTableQeury.value =
-        JSON.parse(sessionStorage.getItem('searchTableQeury') as string) || {}
-      if (searchTableQeury.value.f_etd_start) {
-        DateStart.value = [
-          dayjs(searchTableQeury.value.f_etd_start).format(valueFormatDate),
-          dayjs(searchTableQeury.value.f_etd_end).format(valueFormatDate)
-        ]
-        daterangeObj.ETD =
-          DateStart.value[0].format(formatDate) + ' To ' + DateStart.value[1].format(formatDate)
-        const obj = {
-          title: 'ETD',
-          data: [
-            DateStart.value[0].format(valueFormatDate),
-            DateStart.value[1].format(valueFormatDate)
-          ]
-        }
-        daterangeObj2.ETD = obj
-      }
-      if (searchTableQeury.value.m_eta_start) {
-        DateEnd.value = [
-          dayjs(searchTableQeury.value.m_eta_start),
-          dayjs(searchTableQeury.value.m_eta_end)
-        ]
-        daterangeObj.ETA =
-          DateEnd.value[0].format(formatDate) + ' To ' + DateEnd.value[1].format(formatDate)
-        const obj = {
-          title: 'ETA',
-          data: [DateEnd.value[0].format(valueFormatDate), DateEnd.value[1].format(valueFormatDate)]
-        }
-        daterangeObj2.ETA = obj
-      }
-      if (searchTableQeury.value.created_time_start) {
-        DateCreation.value = [
-          dayjs(searchTableQeury.value.created_time_start),
-          dayjs(searchTableQeury.value.created_time_end)
-        ]
-        daterangeObj['Creation Time'] =
-          DateCreation.value[0].format(formatDate) +
-          ' To ' +
-          DateCreation.value[1].format(formatDate)
-        const obj = {
-          title: 'Creation Time',
-          data: [
-            DateCreation.value[0].format(valueFormatDate),
-            DateCreation.value[1].format(valueFormatDate)
-          ]
-        }
-        daterangeObj2['Creation Time'] = obj
-      }
-    }
-    emit('defaultDate', daterangeObj, daterangeObj2, searchTableQeury.value)
+    filtersStore.deleteFilterByTitle('ETA')
   }
-}
-const daterangedata = ref()
-daterangedata.value = []
-let daterangeObj2: any = {}
-const DateRangeChange = (val: any) => {
-  if (val.data != null) {
-    const date1 = dayjs(String(val.data[0])).format(formatDate)
-    const date2 = dayjs(String(val.data[1])).format(formatDate)
-    daterangeObj[val.title] = date1 + ' To ' + date2
-    daterangeObj2[val.title] = val
+
+  if (hasValidDate(etdDateRange.value)) {
+    filtersStore.updateFilter({
+      title: 'ETD',
+      keyType: 'dateRange',
+      value: etdDateRange.value,
+      key: etdKey
+    })
   } else {
-    delete daterangeObj[val.title]
-    delete daterangeObj2[val.title]
-    if (val.title == 'ETD') {
-      DateStart.value = []
-    } else if (val.title == 'ETA') {
-      DateEnd.value = []
+    filtersStore.deleteFilterByTitle('ETD')
+  }
+
+  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)
     }
-  }
+  })
+  emit('DateRangeSearch')
+  popoverVisible.value = false
 }
-const emit = defineEmits(['DateRangeSearch', 'clearDaterangeTags', 'defaultDate'])
-const DateRangeSearch = () => {
-  emit('DateRangeSearch', daterangeObj, daterangeObj2)
-  Date_visible.value = false
-}
-const CalendarTitle = ref('Date Range')
-watch(
-  () => DateType.value,
-  (current) => {
-    CalendarTitle.value = current
-  }
-)
+
 // 清除
-const clearrest = () => {
+const clearRest = () => {
+  etdDateRange.value = []
+  etaDateRange.value = []
+  otherDateType.value = []
+  filtersStore.deleteFilterByTitle('ETA')
+  filtersStore.deleteFilterByTitle('ETD')
+  allOtherType.value.forEach((item) => {
+    filtersStore.deleteFilterByTitle(item.title)
+  })
   emit('clearDaterangeTags')
-  clearDateStart()
-  clearDateEnd()
-  clearDateCreation()
-  clearDaterangeObj()
-}
-// 清除EDT
-const clearDateStart = () => {
-  DateStart.value = []
-  delete daterangeObj['ETD']
-  delete daterangeObj2['ETD']
-}
-// 清除EDA
-const clearDateEnd = () => {
-  DateEnd.value = []
-  delete daterangeObj['ETA']
-  delete daterangeObj2['ETA']
-}
-// 清除Creation Time
-const clearDateCreation = () => {
-  DateCreation.value = []
-  AddDateType.value = []
-  DateType.value = ''
-  CalendarTitle.value = 'Date Range'
-  delete daterangeObj['Creation Time']
-  delete daterangeObj2['Creation Time']
-}
-// 清除 daterangeObj
-const clearDaterangeObj = () => {
-  daterangeObj = {}
-  daterangeObj2 = {}
+}
+
+const closeRset = () => {
+  etdDateRange.value = []
+  etaDateRange.value = []
+  otherDateType.value = []
 }
 </script>
 <template>
   <div class="select">
-    <el-popover trigger="click" :width="400" :visible="Date_visible" popper-class="DaterangeClass">
+    <el-popover
+      trigger="click"
+      :width="400"
+      :visible="popoverVisible"
+      popper-class="DaterangeClass"
+      @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">
@@ -325,20 +150,12 @@ const clearDaterangeObj = () => {
       <div class="date_header">
         <div class="title">Date Range</div>
         <div class="ETD">
-          <CalendarDate
-            CalendarTitle="ETD"
-            :Date="DateStart"
-            @DateRangeChange="DateRangeChange"
-          ></CalendarDate>
+          <VCalendarDate CalendarTitle="ETD" v-model:date="etdDateRange"></VCalendarDate>
         </div>
         <div class="ETA">
-          <CalendarDate
-            CalendarTitle="ETA"
-            :Date="DateEnd"
-            @DateRangeChange="DateRangeChange"
-          ></CalendarDate>
+          <VCalendarDate CalendarTitle="ETA" v-model:date="etaDateRange"></VCalendarDate>
         </div>
-        <div class="AddType" v-for="(item, index) in AddDateType" :key="item">
+        <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>
@@ -351,32 +168,31 @@ const clearDaterangeObj = () => {
             <el-select
               :suffix-icon="IconDropDown"
               placeholder="Please Select Date Type"
-              v-model="DateType"
+              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
-              :CalendarTitle="CalendarTitle"
+            <VCalendarDate
+              :CalendarTitle="item.label || 'Date Range'"
               CalendarWidth="352px"
-              :Date="DateCreation"
-              @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">
-          <div><el-button type="default" @click="clearrest" class="Clear">Reset</el-button></div>
+          <div><el-button type="default" @click="clearRest" class="Clear">Reset</el-button></div>
           <div>
             <el-button class="search el-button--dark" @click="DateRangeSearch">Search</el-button>
           </div>
@@ -467,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;

+ 8 - 20
src/components/DateRange/src/components/CalendarDate.vue

@@ -4,6 +4,7 @@ import { ref, watch } from 'vue'
 import { useUserStore } from '@/stores/modules/user'
 
 const userStore = useUserStore()
+const formatDate = userStore.dateFormat
 const valueFormatDate = 'MM/DD/YYYY'
 // type RangeValue = [Dayjs, Dayjs]
 // const ETDDate = ref<RangeValue>()
@@ -48,8 +49,8 @@ watch(
   (current: any) => {
     if (current?.length == 2) {
       ETDDate.value = [
-        current[0] ? dayjs(current[0]).format(valueFormatDate) : '',
-        current[1] ? dayjs(current[1]).format(valueFormatDate) : ''
+        current[0] ? dayjs(current[0], formatDate).format(valueFormatDate) : '',
+        current[1] ? dayjs(current[1], formatDate).format(valueFormatDate) : ''
       ]
     } else {
       ETDDate.value = []
@@ -76,15 +77,10 @@ const daterange = (val: any) => {
 }
 const ChangeToday = (val: any) => {
   if (val == 'Earliest') {
-    // ETDDate.value = [dayjs(), dayjs()]
-    ETDDate.value[0] = dayjs()
-    const date1 = dayjs(String(ETDDate.value[0])).format(valueFormatDate)
-    DateList.value[0] = date1
+    DateList.value[0] = dayjs().format(valueFormatDate)
     daterange(DateList.value[1])
   } else {
-    ETDDate.value[1] = dayjs()
-    const date1 = dayjs(String(ETDDate.value[1])).format(valueFormatDate)
-    DateList.value[1] = date1
+    DateList.value[1] = dayjs().format(valueFormatDate)
     daterange(DateList.value[0])
   }
 }
@@ -99,15 +95,11 @@ const handleCalendarOpen = (date: any) => {
   }
 }
 const Earliest = () => {
-  ETDDate.value[0] = dayjs('Oct-05-2009')
-  const date1 = dayjs(String(ETDDate.value[0])).format(valueFormatDate)
-  DateList.value[0] = date1
+  DateList.value[0] = dayjs('Oct-05-2009').format(valueFormatDate)
   daterange(DateList.value[1])
 }
 const Latest = () => {
-  ETDDate.value[1] = dayjs()
-  const date1 = dayjs(String(ETDDate.value[1])).format(valueFormatDate)
-  DateList.value[1] = date1
+  DateList.value[1] = dayjs().format(valueFormatDate)
   daterange(DateList.value[0])
 }
 const changeRangeData = (value: any) => {
@@ -128,10 +120,6 @@ const handlePanelChange = (value: any, mode: any) => {
     isShowExtra.value = true
   }
 }
-// 判断失焦时是否两个都有值
-const isTwoDate = (date: any) => {
-  console.log(date)
-}
 </script>
 <template>
   <div>
@@ -143,7 +131,7 @@ const isTwoDate = (date: any) => {
         width: props.CalendarWidth,
         backgroundColor: props.isType ? 'var(--more-type-bg-color)' : 'var(--management-bg-color)'
       }"
-      :popupClassName="{ 'th-color': props.isShowPopupClass }"
+      :popupClassName="props.isShowPopupClass ? 'th-color' : ''"
       :open="open"
       :disabled="Disabled"
       @change="changeRangeData"

+ 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>

Diferenças do arquivo suprimidas por serem muito extensas
+ 151 - 975
src/components/MoreFilters/src/MoreFilters.vue


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

@@ -0,0 +1,242 @@
+<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'
+    },
+    {
+      title: 'Controlling Customer',
+      key: 'customer_name',
+      type: 'apex'
+    }
+  )
+}
+
+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 - 153
src/components/SelectTableSelect/src/SelectTableSelect.vue

@@ -2,172 +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"
-        class="testname"
-        @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>
@@ -175,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>

+ 183 - 4
src/components/ShipmentStatus/src/ShipmentStatus.vue

@@ -22,6 +22,40 @@ watch(
   }
 )
 
+const isShowDeliveryInfo = ref(false)
+const deliveredInfoRef = ref()
+const simplexContentRef = ref()
+const handleSeeAllDeliveryInfo = () => {
+  nextTick(async () => {
+    const container = simplexContentRef.value
+    const elements = deliveredInfoRef.value
+
+    if (!container || !elements) return
+
+    const targetElement = Array.isArray(elements) ? elements[0] : elements
+    if (!targetElement) return
+
+    // 📏 获取布局信息
+    const containerRect = container.getBoundingClientRect()
+    const elementRect = targetElement.getBoundingClientRect()
+
+    const relativeTop = elementRect.top - containerRect.top
+    const scrollTop = container.scrollTop + relativeTop - 30
+
+    const maxScrollTop = container.scrollHeight - container.clientHeight
+    const safeScrollTop = Math.max(0, Math.min(scrollTop, maxScrollTop))
+
+    try {
+      container.scrollTo({
+        top: safeScrollTop,
+        behavior: 'smooth'
+      })
+    } catch (error) {
+      // 💠 兼容性兜底:某些浏览器不支持 'smooth' 滚动
+      container.scrollTop = safeScrollTop
+    }
+  })
+}
 // 中间点 每两个节点之间加上26px  上边距离28px 下边距离54px  如果没有中间点则高度为56px
 const getSimplexLineHeight = (index: number) => {
   if (index === 0) {
@@ -30,17 +64,34 @@ const getSimplexLineHeight = (index: number) => {
     return 28 + 56 + 26 * (index - 1)
   }
 }
+const getDeliverySimplexHeight = (stepItem) => {
+  let curHeight = 0
+  curHeight = 28 + 26 * (stepItem.children?.length > 0 ? stepItem.children.length - 1 : 0)
+  // if (!stepItem.deliveredData?.length) return curHeight
+  // curHeight =
+  //   curHeight + 90 * stepItem.deliveredData?.length + 8 * (stepItem.deliveredData?.length - 1)
+  return curHeight
+}
+const getDeliverySimplexLineHeight = (stepItem) => {
+  if (!stepItem.children?.length) return 0
+  return 28 + 26 * (stepItem.children.length - 1)
+}
+
+const getDeliveryInfoTop = (stepItem) => {
+  if (!stepItem.children?.length) return 0
+  return 30 + 26 * stepItem.children.length
+}
+
 const getDateHeight = (index: number) => {
   return 42 + 26 * index
 }
-
 const pathRef = ref()
 
 const { isOverflow: isPathOverflow } = useOverflow(pathRef, props)
 </script>
 
 <template>
-  <div class="simplex-content">
+  <div class="simplex-content" ref="simplexContentRef">
     <div
       class="detail-step-item"
       :class="{
@@ -73,6 +124,49 @@ const { isOverflow: isPathOverflow } = useOverflow(pathRef, props)
             <div class="label">{{ dateItem.label }}</div>
             <div class="divider"></div>
             <div class="date">{{ formatTimezone(dateItem.date) }}</div>
+            <!-- <el-button
+              @click="isShowDeliveryInfo = !isShowDeliveryInfo"
+              v-if="dateItem.label === 'Delivered'"
+              class="see-all-icon el-button--text"
+            >
+              See All
+              <span class="font_family icon-icon_next_b"></span>
+            </el-button> -->
+            <see-all-icon
+              v-if="dateItem.label === 'Delivered'"
+              v-model="isShowDeliveryInfo"
+              @collapse="handleSeeAllDeliveryInfo"
+            ></see-all-icon>
+          </div>
+          <div
+            :style="{ top: getDeliveryInfoTop(stepItem) + 'px' }"
+            class="delivered-info"
+            style="scroll-margin-top: 20px"
+            v-if="stepItem.label === 'Place of Delivery' && isShowDeliveryInfo"
+            ref="deliveredInfoRef"
+          >
+            <div
+              class="delivered-item"
+              v-for="(deliveredItem, deliveredIndex) in stepItem.deliveredData"
+              :key="deliveredIndex"
+            >
+              <div class="top">
+                <div class="top-left">
+                  <div class="label">{{ deliveredItem.label }}</div>
+                  <div class="date">
+                    {{ formatTimezone(deliveredItem.date, deliveredItem.timezone) }}
+                  </div>
+                </div>
+                <div class="top-right">{{ deliveredItem.location }}</div>
+              </div>
+              <div class="bottom">
+                <span class="font_family icon-icon_container_b"></span>
+                <div style="margin-top: 1px; margin-left: 4px">Container:</div>
+                <div style="margin-left: 4px; margin-top: 3px; font-weight: 700">
+                  {{ deliveredItem.container }}
+                </div>
+              </div>
+            </div>
           </div>
         </div>
       </div>
@@ -81,6 +175,22 @@ const { isOverflow: isPathOverflow } = useOverflow(pathRef, props)
         v-if="stepItem.label !== 'Place of Delivery'"
         :style="{ height: getSimplexLineHeight(stepItem.children?.length || 0) + 'px' }"
       ></div>
+      <div
+        class="place-of-delivery-line"
+        v-if="stepItem.label === 'Place of Delivery'"
+        :style="{ height: getDeliverySimplexHeight(stepItem) + 'px' }"
+      >
+        <div
+          class="dashed-line"
+          :style="{
+            height: getDeliverySimplexLineHeight(stepItem) + 'px',
+            borderStyle: stepItem.isArrival ? 'solid' : 'dashed',
+            borderColor: stepItem.isArrival
+              ? 'var(--color-neutral-2)'
+              : 'var(--color-shipment-status-label-bg)'
+          }"
+        ></div>
+      </div>
     </div>
   </div>
 </template>
@@ -88,8 +198,11 @@ const { isOverflow: isPathOverflow } = useOverflow(pathRef, props)
 <style lang="scss" scoped>
 // 单式样式
 .simplex-content {
+  position: relative;
   width: 100%;
-  padding: 24px 8px 9px 16px;
+  height: 100%;
+  overflow: auto;
+  padding: 24px 8px 24px 16px;
 }
 .detail-step-item {
   & > .data {
@@ -164,6 +277,7 @@ const { isOverflow: isPathOverflow } = useOverflow(pathRef, props)
         gap: 8px;
         width: calc(100% + 20px);
         .label {
+          margin-left: 3px;
           font-weight: 700;
         }
         .divider {
@@ -175,14 +289,79 @@ const { isOverflow: isPathOverflow } = useOverflow(pathRef, props)
         .date {
           font-size: 12px;
         }
+        :deep(.see-all-icon) {
+          width: 52px;
+          height: 24px;
+          padding-top: 3px;
+          span {
+            font-size: 12px;
+          }
+          .btn {
+            margin-left: 2px;
+          }
+        }
+      }
+      .delivered-info {
+        position: absolute;
+        margin-top: 8px;
+        width: 100%;
+        .delivered-item {
+          width: 100%;
+          margin-bottom: 8px;
+          border-radius: 6px;
+          background: var(--color-table-header-bg);
+          &:last-child {
+            margin-bottom: 20px;
+          }
+          .top {
+            display: flex;
+            justify-content: space-between;
+            align-items: center;
+            height: 58px;
+            border-bottom: 1px solid var(--color-border);
+            .top-left {
+              flex: 1;
+              display: flex;
+              flex-direction: column;
+              gap: 4px;
+              padding: 8px;
+              border-right: 1px solid var(--color-border);
+              .label {
+                font-weight: 700;
+              }
+              .date {
+                font-size: 12px;
+              }
+            }
+            .top-right {
+              padding: 0 23px;
+              text-align: center;
+              font-weight: 700;
+              color: var(--color-neutral-1);
+            }
+          }
+          .bottom {
+            display: flex;
+            align-items: center;
+            height: 32px;
+            padding: 8px;
+          }
+        }
       }
     }
   }
   & > .line {
-    height: 72px;
     margin-left: 7px;
     border-left: 1px solid var(--color-neutral-1);
   }
+  & > .place-of-delivery-line {
+    margin-left: 7px;
+    // border-left: 1px dashed var(--color-neutral-3);
+    .dashed-line {
+      height: 50px;
+      border-left: 1px dashed var(--color-neutral-2);
+    }
+  }
   &.last {
     & > .data {
       .left-step-icon {

+ 35 - 0
src/components/TableImgEmpty/TableImgEmpty.vue

@@ -0,0 +1,35 @@
+<script setup lang="ts">
+import { computed } from 'vue'
+import lightPng from './image/empty-light.png'
+import darkPng from './image/empty-dark.png'
+import { useThemeStore } from '@/stores/modules/theme'
+
+const themeStore = useThemeStore()
+// 判断当前系统主题模式
+const emptyImg = computed(() => {
+  return themeStore.theme === 'dark' ? darkPng : lightPng
+})
+
+const props = defineProps({
+  EmptyTitle: String
+})
+</script>
+
+<template>
+  <div class="table-img-empty">
+    <img :src="emptyImg" />
+    <div class="empty-text">No data</div>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.table-img-empty {
+  .empty-text {
+    margin: 8px 0;
+    color: var(--color-neutral-2);
+    text-align: center;
+    font-size: 14px;
+    font-weight: 700;
+  }
+}
+</style>

BIN
src/components/TableImgEmpty/image/empty-dark.png


BIN
src/components/TableImgEmpty/image/empty-light.png


+ 0 - 0
src/components/TableImgEmpty/index.ts


+ 65 - 176
src/components/TransportMode/src/TransportMode.vue

@@ -1,7 +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
@@ -11,200 +13,87 @@ 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)
+  const isCheckedAll = transportList.value?.every((item: any) => item.checked)
+  isCheckedAll ? (checkAll.value = true) : (checkAll.value = false)
+}
 watch(
-  () => props.TransportListItem,
+  () => props.transportListItem,
   (current) => {
-    console.log(current)
-    TransportList.value = current
-    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
-      }
-    })
+    updateTransportList(current)
+  },
+  {
+    immediate: true,
+    deep: true
   }
 )
 
-onMounted(() => {
-  emitter.on('clearTag', (tag: any) => {
-    if (tag.includes('Transport Mode')) {
-      clearList()
-    }
-  })
-  defaultTransport()
+const totalNumber = computed(() => {
+  return (
+    transportList.value?.filter((item) => {
+      return item.checked
+    }).length || 0
+  )
 })
 
-onBeforeMount(() => {
-  emitter.off('clearTag')
-})
-
-const checkAll = ref(false)
-const dropdown1 = 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 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()]
-    }
+}
+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'
   })
-  if (checkedCount.length == TransportList.value.length) {
-    changedata.value = ['All']
-  } else {
-    changedata.value = checkedCount
-  }
-  TransportData.data = changedata.value
-  emit('TransportSearch', TransportData)
-  if (!dropdown1.value) return
-  if (visible) {
-    dropdown1.value.handleClose()
-  } else {
-    dropdown1.value.handleOpen()
-  }
+  dropdownVisible.value.handleClose()
+  emit('transportSearch', transportList.value)
 }
-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 handleDropdownVisibleChange = (visible: boolean) => {
+  if (visible) {
+    updateTransportList(props.transportListItem)
   }
 }
 </script>
 <template>
   <div class="select">
-    <el-dropdown ref="dropdown1" trigger="click" :hide-on-click="false">
+    <el-dropdown
+      ref="dropdownVisible"
+      trigger="click"
+      :hide-on-click="false"
+      @visible-change="handleDropdownVisibleChange"
+    >
       <div class="el-dropdown-link">
         <div class="select_title">Transport Mode</div>
         <span class="iconfont_icon">
@@ -227,16 +116,16 @@ const defaultTransport = () => {
                   </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">
@@ -254,7 +143,7 @@ const defaultTransport = () => {
                 <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>
@@ -308,7 +197,7 @@ const defaultTransport = () => {
   padding-left: 16px;
 }
 .checkbox {
-  width: 216px;
+  width: 100%;
   font-weight: 400;
   font-size: var(--font-size-3);
   padding: 0 12px;
@@ -319,7 +208,7 @@ const defaultTransport = () => {
   width: 0;
 }
 :deep(.el-checkbox__label) {
-  width: 176px;
+  width: 100%;
   padding-left: 13px;
   display: flex;
   align-items: center;

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

@@ -17,31 +17,31 @@ const handleGoBack = () => {
 }
 let monitoringQuery = ref()
 const jumpLink = (label: string, query: any) => {
-    if(label == 'Monitoring Settings') {
-      CancelRulesVisible.value = true
-      monitoringQuery.value = query
-    } else if(label == 'Destination Delivery') {
-      router.push({
-        name: 'Destination Delivery',
-        query: query
-      })
-    } else if (label == 'Configurations'){
-      router.push({
-        name: 'Configurations',
-        query: query
-      })
-    }else {
-      label &&
+  if (label == 'System Settings') {
+    CancelRulesVisible.value = true
+    monitoringQuery.value = query
+  } else if (label == 'Destination Delivery') {
+    router.push({
+      name: 'Destination Delivery',
+      query: query
+    })
+  } else if (label == 'Configurations') {
+    router.push({
+      name: 'Configurations',
+      query: query
+    })
+  } else {
+    label &&
       router.push({
         name: label,
         query: query
       })
-    }
+  }
 }
 const jumpLinkMonitoring = () => {
   CancelRulesVisible.value = false
   router.push({
-    name: 'Monitoring Settings',
+    name: 'System Settings',
     query: monitoringQuery.value
   })
 }
@@ -57,39 +57,39 @@ 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>
-    <!-- 取消保存 -->
-    <el-dialog v-model="CancelRulesVisible" width="480">
-      <div style="font-weight: 400">You have unsaved changes.</div>
-      <div style="font-weight: 400">Are you sure you want to leave this page?</div>
-      <template #footer>
-        <div class="dialog-footer">
-          <el-button type="default" @click="CancelRulesVisible = false" style="width: 100px"
-            >Cancel</el-button
-          >
-          <el-button class="el-button--warning" @click="jumpLinkMonitoring" style="width: 100px">
-            OK
-          </el-button>
-        </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>
-          Unsaved Changes
-        </div>
-      </template>
-    </el-dialog>
+  <!-- 取消保存 -->
+  <el-dialog v-model="CancelRulesVisible" width="480">
+    <div style="font-weight: 400">You have unsaved changes.</div>
+    <div style="font-weight: 400">Are you sure you want to leave this page?</div>
+    <template #footer>
+      <div class="dialog-footer">
+        <el-button type="default" @click="CancelRulesVisible = false" style="width: 100px"
+          >Cancel</el-button
+        >
+        <el-button class="el-button--warning" @click="jumpLinkMonitoring" style="width: 100px">
+          OK
+        </el-button>
+      </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>
+        Unsaved Changes
+      </div>
+    </template>
+  </el-dialog>
 </template>
 
 <style lang="scss" scoped>

+ 1 - 0
src/components/VEllipsisTooltip/index.ts

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

+ 212 - 0
src/components/VEllipsisTooltip/src/VEllipsisTooltip.vue

@@ -0,0 +1,212 @@
+<template>
+  <el-tooltip
+    :disabled="!shouldShowTooltip"
+    :content="tooltipContent"
+    :popper-class="popperClass"
+    :placement="placement"
+    v-bind="tooltipProps"
+    ref="tooltipRef"
+  >
+    <div
+      ref="containerRef"
+      class="ellipsis-container"
+      :class="{ 'is-clamp-multi': lineClamp > 1 }"
+      :style="finalContainerStyle"
+      @mouseenter="handleMouseEnter"
+      @mouseleave="handleMouseLeave"
+    >
+      <slot>
+        <span ref="textRef" class="ellipsis-text" :style="textStyle">
+          {{ content }}
+        </span>
+      </slot>
+    </div>
+  </el-tooltip>
+</template>
+
+<script setup>
+import { ref, computed, onMounted, onUnmounted, watch, nextTick } from 'vue'
+
+const props = defineProps({
+  content: String,
+  maxWidth: {
+    type: Number,
+    default: 200
+  },
+  maxHeight: {
+    type: [Number, String],
+    default: ''
+  },
+  lineClamp: {
+    type: Number,
+    default: 1
+  },
+  tooltipProps: {
+    type: Object,
+    default: () => ({})
+  },
+  popperClass: {
+    type: String,
+    default: 'ellipsis-tooltip'
+  },
+  placement: {
+    type: String,
+    default: 'top'
+  },
+  containerStyle: {
+    type: Object,
+    default: () => ({})
+  },
+  textStyle: {
+    type: Object,
+    default: () => ({})
+  }
+})
+
+// --- DOM refs ---
+const containerRef = ref(null)
+const textRef = ref(null)
+const tooltipRef = ref(null)
+
+// --- 是否应显示 tooltip ---
+const shouldShowTooltip = ref(false)
+
+// --- 动态计算样式类 ---
+const containerClasses = computed(() => ({
+  'is-clamp-multi': props.lineClamp > 1
+}))
+
+// --- 实际用于 Tooltip 显示的内容 ---
+const tooltipContent = computed(() => props.content || '')
+
+// --- 容器样式(max-width / max-height)---
+const finalContainerStyle = computed(() => ({
+  maxWidth: props.maxWidth + 'px',
+  maxHeight: props.maxHeight
+    ? typeof props.maxHeight === 'number'
+      ? props.maxHeight + 'px'
+      : props.maxHeight
+    : 'none',
+  ...props.containerStyle
+}))
+
+// --- 文本样式(根据 lineClamp 应用不同 CSS)---
+const finalTextStyle = computed(() => {
+  const style = { ...props.textStyle }
+  if (props.lineClamp <= 1) {
+    return {
+      display: 'block',
+      overflow: 'hidden',
+      textOverflow: 'ellipsis',
+      whiteSpace: 'nowrap',
+      ...style
+    }
+  } else {
+    return {
+      display: '-webkit-box',
+      WebkitBoxOrient: 'vertical',
+      WebkitLineClamp: props.lineClamp,
+      overflow: 'hidden',
+      textOverflow: 'ellipsis',
+      wordBreak: 'break-all', // 多行建议启用
+      ...style
+    }
+  }
+})
+
+// --- 检查是否溢出 ---
+const checkOverflow = async () => {
+  await nextTick() // 确保 DOM 更新
+  const container = containerRef.value
+  const textEl = textRef.value || container?.querySelector('.ellipsis-text')
+
+  if (!container || !textEl) return
+
+  let isOverflowing = false
+
+  if (props.lineClamp <= 1) {
+    isOverflowing = textEl.scrollWidth > container.clientWidth
+  } else {
+    // 多行看高度是否溢出(line-clamp 截断)
+    isOverflowing = textEl.scrollHeight > container.clientHeight
+  }
+
+  shouldShowTooltip.value = isOverflowing
+}
+
+// --- 鼠标事件用于手动触发检查(防抖)---
+let resizeTimer
+const handleMouseEnter = () => {
+  clearTimeout(resizeTimer)
+  resizeTimer = setTimeout(checkOverflow, 50)
+}
+
+// 可选:也可监听 resize
+onMounted(() => {
+  window.addEventListener('resize', checkOverflow)
+  checkOverflow()
+})
+
+onUnmounted(() => {
+  window.removeEventListener('resize', checkOverflow)
+  clearTimeout(resizeTimer)
+})
+
+// 监听 props 变化
+watch(
+  () => [props.content, props.lineClamp, props.maxWidth, props.maxHeight],
+  () => {
+    checkOverflow()
+  },
+  { immediate: true }
+)
+</script>
+
+<style lang="scss" scoped>
+.ellipsis-container {
+  display: inline-block;
+  max-width: v-bind('finalContainerStyle.maxWidth');
+  max-height: v-bind('finalContainerStyle.maxHeight');
+  overflow: hidden;
+  vertical-align: top;
+  line-height: 32px;
+}
+
+.ellipsis-text {
+  width: 100%; // 利用父容器宽度
+}
+.ellipsis-container {
+  display: inline-block;
+  max-width: v-bind('finalContainerStyle.maxWidth');
+  max-height: v-bind('finalContainerStyle.maxHeight');
+  overflow: hidden;
+  line-height: 1.5;
+}
+
+.ellipsis-text {
+  width: 100%;
+  display: block;
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+
+.ellipsis-container.is-clamp-multi .ellipsis-text {
+  display: -webkit-box;
+  line-clamp: v-bind('lineClamp');
+  -webkit-box-orient: vertical;
+  -webkit-line-clamp: v-bind('lineClamp');
+  white-space: normal;
+  word-break: break-all;
+}
+</style>
+
+<!-- 全局样式建议提取到全局 SCSS 文件 -->
+<style>
+.ellipsis-tooltip {
+  max-width: 200px;
+  word-break: break-word;
+  white-space: pre-line;
+  margin-bottom: -15px;
+}
+</style>

+ 29 - 11
src/components/VTag/src/VTag.vue

@@ -13,6 +13,8 @@ interface internalProps {
     | 'Pending Approval'
     | 'Approved'
     | 'Rejected'
+    | 'Active'
+    | 'Inactive'
   large?: boolean
 }
 
@@ -24,11 +26,13 @@ const mappingTable = new Map([
   ['Created', 'created'],
   ['Booked', 'booked'],
   ['Cargo Received', 'cargo-received'],
-  ['Departure', 'departure'],
+  // ['Departure', 'departure'],
   ['Arrived', 'arrived'],
   ['Completed', 'completed'],
-  ['Departed', 'Departed'],
-  ['Pending Approval', 'pending-approval']
+  ['Departed', 'departed'],
+  ['Pending Approval', 'pending-approval'],
+  ['Active', 'active'],
+  ['Inactive', 'inactive']
 ])
 defineProps<internalProps>()
 </script>
@@ -100,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 {
@@ -121,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);
@@ -135,6 +139,20 @@ defineProps<internalProps>()
       background-color: var(--color-tag-unfinished-approval);
     }
   }
+  &.v-tag__active {
+    background-color: var(--color-tag-completed-bg);
+    color: var(--color-tag-completed);
+    .dot {
+      background-color: var(--color-tag-completed);
+    }
+  }
+  &.v-tag__inactive {
+    background-color: var(--color-tag-cancelled-bg);
+    color: var(--color-tag-cancelled);
+    .dot {
+      background-color: var(--color-tag-cancelled);
+    }
+  }
   &.v-tag__approved {
     background-color: var(--color-tag-approved-bg);
     color: var(--color-tag-approved);

+ 176 - 196
src/components/selectAutoSelect/src/selectAutoSelect.vue

@@ -1,252 +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"
-        class="testname"
-        @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>
@@ -254,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;
@@ -312,4 +292,4 @@ const typeSelectClick = (index: any, val: any) => {
   line-height: 14px;
   margin-top: 5px;
 }
-</style>
+</style>

+ 8 - 8
src/hooks/calculatingHeight.ts

@@ -15,7 +15,7 @@ export const useCalculatingHeight = (
     if (parentElement) {
       containerHeight.value = parentElement.offsetHeight - fixedHeight
       elementList.forEach((item: any) => {
-        if (item.value) {
+        if (item?.value) {
           containerHeight.value -= item.value.offsetHeight || 0
         }
       })
@@ -32,19 +32,19 @@ export const useCalculatingHeight = (
       resizeObserver.observe(parentElement)
     }
     elementList.forEach((item) => {
-      if (item.value) {
+      if (item?.value) {
         resizeObserver.observe(item.value)
       }
     })
   }
 
   // Vue 生命周期钩子
-  onMounted(() => {
-    nextTick(() => {
-      calculatingHeight()
-      startObserving()
-    })
-  })
+  // onMounted(() => {
+  //   nextTick(() => {
+  //     calculatingHeight()
+  //     startObserving()
+  //   })
+  // })
 
   // 停止观察
   const stopObserving = () => {

+ 4 - 0
src/main.ts

@@ -25,6 +25,7 @@ import { createPinia } from 'pinia'
 import App from './App.vue'
 import router from './router'
 import { useThemeStore } from '@/stores/modules/theme'
+import piniaPluginPersistedstate from 'pinia-plugin-persistedstate';
 
 const app = createApp(App)
 
@@ -44,6 +45,9 @@ VxeUIAll.VxeUI.setConfig({
 })
 
 app.use(createPinia())
+const pinia = createPinia()
+pinia.use(piniaPluginPersistedstate)
+app.use(pinia)
 app.use(VXETable)
 app.use(VxeUI)
 app.use(router)

+ 133 - 27
src/router/index.ts

@@ -1,6 +1,8 @@
 import { createRouter, createWebHistory } from 'vue-router'
 import { useUserStore } from '@/stores/modules/user'
 import { useBreadCrumb } from '@/stores/modules/breadCrumb'
+import { useHeaderSearch } from '@/stores/modules/headerSearch'
+import { useFiltersStore } from '@/stores/modules/filtersList'
 
 const router = createRouter({
   history: createWebHistory(`${import.meta.env.VITE_BASE_URL}`),
@@ -21,6 +23,29 @@ const router = createRouter({
           name: 'Booking',
           component: () => import('../views/Booking')
         },
+        {
+          path: '/report',
+          name: 'Report Management',
+          component: () => import('../views/Report')
+        },
+        {
+          path: '/report/detail',
+          name: 'Report Detail',
+          component: () => import('../views/Report/src/components/ReportDetail'),
+          meta: {
+            activeMenu: '/report',
+            breadName: 'Detail'
+          }
+        },
+        {
+          path: '/report/schedule',
+          name: 'Report Schedule',
+          component: () => import('../views/Report/src/components/ReportSchedule'),
+          meta: {
+            activeMenu: '/report',
+            breadName: 'Schedule Configuration'
+          }
+        },
         {
           path: '/booking/detail',
           name: 'Booking Detail',
@@ -42,6 +67,14 @@ const router = createRouter({
             activeMenu: '/tracking'
           }
         },
+        {
+          path: '/tracking/download-attachment',
+          name: 'Tracking Download Attachment',
+          component: () => import('../views/Tracking/src/components/DownloadAttachment'),
+          meta: {
+            activeMenu: '/tracking'
+          }
+        },
         {
           path: '/shipment/detail',
           name: 'Shipment Detail',
@@ -92,8 +125,8 @@ const router = createRouter({
           component: () => import('../views/Login/src/components/ChangePasswordCard.vue')
         },
         {
-          path: '/Operationlog',
-          name: 'Operationlog',
+          path: '/operation-log',
+          name: 'Operation log',
           component: () => import('../views/OperationLog')
         },
         {
@@ -107,10 +140,23 @@ const router = createRouter({
           component: () => import('../views/AIApiLog')
         },
         {
-          path: '/PromptConfiguration',
-          name: 'PromptConfiguration',
+          path: '/prompt-configuration',
+          name: 'Prompt Configuration',
           component: () => import('../views/PromptConfiguration')
         },
+        {
+          path: '/template-management',
+          name: 'Template Management',
+          component: () => import('../views/TemplateManagement')
+        },
+        {
+          path: '/template-management/create-report-template',
+          name: 'Create Report Template',
+          component: () => import('../views/TemplateManagement/src/components/CreateReportTemplate'),
+          meta: {
+            activeMenu: '/template-management'
+          }
+        },
         {
           path: '/system-message',
           name: 'System Message',
@@ -126,21 +172,16 @@ const router = createRouter({
           component: () => import('../views/SystemMessage/src/components/SystemMessageDetail.vue')
         },
         {
-          path: '/SystemSettings',
-          name: 'Monitoring Settings',
-          component: () => import('../views/SystemSettings')
-        },
-        {
-          path: '/SystemSettings',
+          path: '/system-settings',
           name: 'System Settings',
           component: () => import('../views/SystemSettings')
         },
         {
-          path: '/SystemSettings/createnewrule',
+          path: '/system-settings/create-new-rule',
           name: 'Create New Rule',
           component: () => import('../views/SystemSettings/src/components/CreateNewrule'),
           meta: {
-            activeMenu: '/SystemSettings'
+            activeMenu: '/system-settings'
           }
         },
         {
@@ -149,29 +190,34 @@ const router = createRouter({
           component: () => import('../views/DestinationDelivery')
         },
         {
-          path: '/destination-delivery/CreateNewBooking',
+          path: '/destination-delivery/create-new-booking',
           name: 'Create New Booking',
           component: () => import('../views/DestinationDelivery/src/components/CreateNewBooking')
         },
         {
-          path: '/destination-delivery/ConfiguRations',
+          path: '/destination-delivery/configurations',
           name: 'Configurations',
-          component: () => import('../views/DestinationDelivery/src/components/ConfiguRations')
+          component: () => import('../views/DestinationDelivery/src/components/ConfiguRations'),
+          meta: {
+            activeMenu: '/destination-delivery'
+          }
         },
         {
-          path: '/destination-delivery/ConfiguRations/CreateNewRule',
+          path: '/destination-delivery/configurations/create-new-rule',
           name: 'Destination Create New Rule',
-          component: () => import('../views/DestinationDelivery/src/components/ConfiguRations/src/components/CreateNewRule.vue')
-        },
-        {
-          path: '/destination-delivery/modify-booking',
-          name: 'Modify Booking',
-          component: () => import('../views/DestinationDelivery/src/components/ModifyBooking'),
+          component: () =>
+            import(
+              '../views/DestinationDelivery/src/components/ConfiguRations/src/components/CreateNewRule.vue'
+            ),
           meta: {
-            breadName: 'Modify Booking',
             activeMenu: '/destination-delivery'
           }
-        }
+        },
+        {
+          path: '/demo-video',
+          name: 'Demo Video',
+          component: () => import('../views/Video'),
+        },
       ]
     }
   ]
@@ -179,12 +225,13 @@ const router = createRouter({
 
 // * 路由拦截 beforeEach
 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')
@@ -207,8 +254,66 @@ router.beforeEach(async (to, from, next) => {
     sessionStorage.removeItem('activeCardTypeName')
   }
 
+  const SHOULD_CLEAR_FILTERS = (to, from) => {
+    const PATHS = {
+      TRACKING_HOME: '/tracking',
+      BOOKING_HOME: '/booking',
+      TRACKING_DETAIL: '/tracking/detail',
+      BOOKING_DETAIL: '/booking/detail',
+      TRACKING_ADD_VGM: '/tracking/add-vgm',
+    };
+
+    const PAGE_NAMES = {
+      BOOKING_DETAIL: 'Booking Detail',
+      TRACKING_DETAIL: 'Tracking Detail',
+      ADD_VGM: 'Add VGM',
+    };
+    const breadCrumbStore = useBreadCrumb();
+
+    // 安全获取面包屑根路径
+    const rootPath = breadCrumbStore.routeList?.[0]?.path;
+
+    // --- 定义“应该保留筛选器”的场景 (Keep Filters) ---
+
+    // 场景 1: 从详情页返回列表页,且列表页是面包屑根节点 (用户只是进去看了一眼详情又回来了)
+    const isReturningFromDetail =
+      [PATHS.TRACKING_DETAIL, PATHS.BOOKING_DETAIL].includes(from.path) &&
+      [PATHS.TRACKING_HOME, PATHS.BOOKING_HOME].includes(to.path) &&
+      rootPath === to.path;
+
+    // 场景 2: 从 VGM 页返回 Tracking 列表页
+    const isReturningFromVgm =
+      from.path === PATHS.TRACKING_ADD_VGM &&
+      to.path === PATHS.TRACKING_HOME;
+
+    // 场景 3: 从列表页进入白名单子页面 (用户正准备进去看详情,稍后会回来)
+    // 注意:这里假设 to.name 是可靠的,否则建议也用 path 映射
+    const isEnteringWhitelist =
+      (from.path === PATHS.TRACKING_HOME && [PAGE_NAMES.BOOKING_DETAIL, PAGE_NAMES.TRACKING_DETAIL, PAGE_NAMES.ADD_VGM].includes(to.name)) ||
+      (from.path === PATHS.BOOKING_HOME && [PAGE_NAMES.BOOKING_DETAIL, PAGE_NAMES.TRACKING_DETAIL].includes(to.name));
+
+    // 场景 4:页面刷新
+    const isRefresh = from.path === '/' && (to.path === '/tracking' || to.path === '/booking');
+    // --- 最终逻辑 ---
+    // 如果满足上述任一“保留”场景,则 NOT 清除 (返回 false)
+    // 否则,清除 (返回 true)
+
+    console.log('路由守卫判断:', from)
+    console.log('路由TO', to)
+    const shouldKeepFilters = isReturningFromDetail || isReturningFromVgm || isEnteringWhitelist || isRefresh;
+
+    return !shouldKeepFilters;
+  };
+
+  // --- 使用处 ---
+  if (SHOULD_CLEAR_FILTERS(to, from)) {
+    const filtersListStore = useFiltersStore();
+    filtersListStore.clearFilters();
+  }
+
+
   // 未登录白名单
-  const whiteList = ['/login', '/public-tracking', '/public-tracking/detail', '/reset-password']
+  const whiteList = ['/login', '/public-tracking', '/public-tracking/detail', '/reset-password', '/demo-video']
   // 判断是否登录
   if (!whiteList.includes(to.path) && !userStore.isLogin) {
     const userStore = useUserStore()
@@ -224,7 +329,8 @@ router.beforeEach(async (to, from, next) => {
       next('/login')
     }
   }
+
+  useBreadCrumb().setRouteList(to)
   next()
 })
-
 export default router

+ 37 - 82
src/stores/modules/breadCrumb.ts

@@ -1,7 +1,8 @@
 import { defineStore } from 'pinia'
 
 interface Route {
-  label: string
+  breadName: string
+
   path: string
   query?: string
 }
@@ -12,6 +13,7 @@ interface BreadCrumb {
 const whiteList = [
   'Booking Detail',
   'Tracking Detail',
+  'Tracking Download Attachment',
   'Add VGM',
   'Public Tracking Detail',
   'Create New Rule',
@@ -19,6 +21,13 @@ const whiteList = [
   'Configurations',
   'Create New Booking',
   'Destination Create New Rule',
+  'Create Report Template',
+  'Report Schedule',
+  'Report Detail',
+  'Modify Booking',
+  'System Message Detail',
+  'Public Tracking Detail',
+  'Configurations'
 ]
 
 export const useBreadCrumb = defineStore('breadCrumb', {
@@ -29,119 +38,64 @@ export const useBreadCrumb = defineStore('breadCrumb', {
   actions: {
     setRouteList(toRoute: any) {
       const index = this.routeList.findIndex((item) => item.label === toRoute.name)
+      // 返回之前的路由时,删除之后的所有路由
       if (index !== -1) {
         this.routeList.splice(index + 1)
-        if (toRoute.name === 'Configurations') {
-          this.routeList = [
-            {
-              label: 'Destination Delivery',
-              path: '/destination-delivery',
-              query: ''
-            },
-            {
-              label: 'Configurations',
-              path: '/destination-delivery/ConfiguRations',
-              query: toRoute.query
-            }
-          ]
-        }
-      } else if (toRoute.name === 'Public Tracking Detail') {
-        this.routeList = [
-          {
-            label: 'Public Tracking',
-            path: '/public-tracking',
-            query: ''
-          },
-          {
-            label: 'Public Tracking Detail',
-            path: '/public-tracking/detail',
-            query: toRoute.query
-          }
-        ]
-      } else if (toRoute.name === 'System Message Detail') {
-        this.routeList = [
-          {
-            label: 'System Message',
-            path: '/system-message',
-            query: ''
-          },
-          {
-            label: 'System Message Detail',
-            path: '/system-message/detail',
-            query: toRoute.query
-          }
-        ]
-      } else if (toRoute.name === 'Shipment Detail') {
-        this.routeList = [
-          {
-            label: 'System Settings',
-            path: '/SystemSettings',
-            query: ''
-          },
-          {
-            label: 'Shipment Detail',
-            path: '/shipment/detail',
-            query: toRoute.query
-          }
-        ]
-      } else if (toRoute.name === 'Modify Booking') {
-        this.routeList = [
-          {
-            label: 'Destination Delivery',
-            path: '/destination-delivery',
-            query: ''
-          },
-          {
-            label: 'Modify Booking',
-            path: '/destination-delivery/modify-booking',
-            query: toRoute.query
-          }
-        ]
-      } else if (toRoute.name === 'Destination Create New Rule') {
-        let label = ''
-        if(toRoute.query.a != undefined) {
-          label = 'Modify Rule'
+        return
+      }
+      if (toRoute.name === 'Destination Create New Rule') {
+        let breadName = ''
+        if (toRoute.query.a != undefined) {
+          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',
+            path: '/destination-delivery/configurations',
             query: ''
           },
           {
-            label: label,
-            path: '/destination-delivery/CreateNewRule',
+            breadName: breadName,
+            label: toRoute.name,
+            path: '/destination-delivery/create-new-rule',
             query: toRoute.query
           }
         ]
-      } else if (toRoute.name === 'Create New Booking') {let label = ''
-        if(toRoute.query.a != undefined) {
-          label = 'Modify Booking'
+      } else if (toRoute.name === 'Create New Booking') {
+        let breadName = ''
+        if (toRoute.query.a != undefined) {
+          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,
-            path: '/destination-delivery/CreateNewBooking',
+            breadName: breadName,
+            label: toRoute.name,
+            path: '/destination-delivery/create-new-booking',
             query: toRoute.query
           }
         ]
+
       } 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
         })
@@ -149,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
-    }
-  }
-})

+ 14 - 0
src/stores/modules/notificationMessage.ts

@@ -59,6 +59,20 @@ export const useNotificationMessage = defineStore('notificationMessage', {
         }
       })
     },
+    // 在轮播后将消息置为已读
+    async markMessageAsReadAfterCarousel(displayedIds: string[]) {
+      if (displayedIds.length === 0) return
+
+      await $api.setMessageRead({ id: displayedIds }).then((res) => {
+        if (res.code === 200) {
+          this.readCardMap = []
+          localStorage.setItem('readCardMap', JSON.stringify(this.readCardMap))
+
+          // 在将消息标记为已读后,再次检查是否有新消息
+          this.hasUnreadMessages()
+        }
+      })
+    },
     clearData() {
       this.notificationMsgList = []
       this.readCardMap = []

+ 20 - 0
src/stores/modules/trackingDownloadData.ts

@@ -0,0 +1,20 @@
+// stores/useDataStore.js
+import { defineStore } from 'pinia';
+
+export const useTrackingDownloadData = defineStore('trackingDownloadData', {
+  state: () => ({
+    serialNoArr: [],
+    schemasArr: []
+  }),
+  actions: {
+    setData(serial_no_arr: string[], schemas_arr: string[]) {
+      this.serialNoArr = serial_no_arr;
+      this.schemasArr = schemas_arr;
+    }
+  },
+  // 持久化配置
+  persist: {
+    storage: sessionStorage // 关闭浏览器就清掉
+    // 或 storage: localStorage // 永久保留
+  }
+});

+ 35 - 3
src/stores/modules/user.ts

@@ -15,10 +15,18 @@ interface UserInfo {
   numbers_format: string
   PASSWORD_CHANGE_CYCLE: number // 密码修改周期(多少天需要改一次)
   last_pwd_change: string // 上次密码修改时间
+  is_desensitization_kln?: string // Mask Customer Information
+  kam_customers_name: Array<{ kam_customers_name: string }>
+}
+interface CustomerInfo {
+  name: string
+  isShowMapping: boolean
 }
 interface UserState {
   userInfo: UserInfo
   isFirstLogin: boolean
+  originalUName: string
+  customerInfo: CustomerInfo
 }
 
 /**
@@ -53,7 +61,9 @@ export const useUserStore = defineStore('user', {
     userInfo: JSON.parse(localStorage.getItem('userInfo') || '{}'),
     isFirstLogin: localStorage.getItem('isFirstLogin')
       ? JSON.parse(localStorage.getItem('isFirstLogin'))
-      : false
+      : false,
+    originalUName: localStorage.getItem('originalUName') || JSON.parse(localStorage.getItem('userInfo') || '{}')?.uname || '',
+    customerInfo: JSON.parse(localStorage.getItem('customerInfo') || '{"name": "", "isShowMapping": false}')
   }),
   getters: {
     userName(state) {
@@ -63,6 +73,10 @@ export const useUserStore = defineStore('user', {
         return state.userInfo.uname || ''
       }
     },
+    isShowMapping(state) {
+      return state.customerInfo.isShowMapping
+    },
+
     dateFormat(state) {
       return state.userInfo.date_format || getDateFormat()
     },
@@ -75,21 +89,39 @@ export const useUserStore = defineStore('user', {
     }
   },
   actions: {
-    setUserInfo(userInfo: any, isFirstLogin?: boolean) {
+    // isFirstLogin字段是
+    /**
+     * 
+     * @param userInfo 
+     * @param isFromMapping 用来判断是否从映射的时候获取的用户信息
+     * @param isFirstLogin 用来判断是不是该用户第一次使用,如果是第一次使用,那么需要跳转到修改密码页面修改密码之后才能继续使用系统
+     */
+    setUserInfo(userInfo: any, isFromMapping?: boolean, isFirstLogin?: boolean) {
       this.userInfo = userInfo
       localStorage.setItem('userInfo', JSON.stringify(userInfo))
-
+      if (!isFromMapping) {
+        this.originalUName = userInfo.uname
+        localStorage.setItem('originalUName', userInfo.uname)
+      }
       if (isFirstLogin !== undefined) {
         localStorage.setItem('isFirstLogin', JSON.stringify(isFirstLogin))
         this.isFirstLogin = isFirstLogin
       }
     },
 
+    setCustomerInfo(customerInfo: CustomerInfo) {
+      this.customerInfo = customerInfo
+      localStorage.setItem('customerInfo', JSON.stringify(customerInfo))
+    },
     async logout(isNeedLogout: boolean = true) {
       if (isNeedLogout) {
         await $api.logout().then(() => { })
       }
       localStorage.removeItem('userInfo')
+      localStorage.removeItem('originalUName')
+      localStorage.removeItem('customerInfo')
+      this.originalUName = ''
+      this.customerInfo = { name: '', isShowMapping: false }
       this.userInfo = {}
       if (localStorage.getItem('isFirstLogin')) {
         localStorage.removeItem('isFirstLogin')

+ 117 - 18
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
@@ -167,9 +183,7 @@ tr
 .ant-picker .ant-picker-input > input::placeholder {
   color: var(--color-neutral-1);
 }
-.ant-picker-dropdown .ant-picker-cell .ant-picker-cell-inner {
-  color: var(--color-neutral-1);
-}
+
 .ant-picker-dropdown .ant-picker-header {
   border-bottom: 1px solid var(--border-color-2);
 }
@@ -192,14 +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);
+  }
+}
+.ant-picker-dropdown  div.ant-picker-cell-inner{
+  color: var(--color-el-date-prev);
+}
+
+.ant-picker-dropdown .ant-picker-cell-in-view .ant-picker-cell-inner{
+  color: var(--color-neutral-1);
 }
+
 .ant-checkbox-checked .ant-checkbox-inner {
   background-color: var(--color-theme) !important;
   border-color: var(--color-theme) !important;
@@ -209,4 +236,76 @@ tr
 }
 .ant-checkbox-checked:after{
   border-color: var(--color-theme) !important;
-}
+}
+.ant-btn-primary {
+  background-color: var(--color-theme) !important;
+  span {
+    color: var(--color-white);
+  }
+  &:hover {
+    background-color: var(--color-btn-main-bg-hover);
+    color: var(--color-white);
+  }
+}
+.ant-picker-dropdown .ant-picker-time-panel-column >li.ant-picker-time-panel-cell-selected .ant-picker-time-panel-cell-inner {
+  background-color: var(--color-orange-6);
+  color: var(--color-theme);
+  &:hover {
+    background-color: var(--color-orange-6);
+  }
+}
+.ant-picker-dropdown .ant-picker-time-panel-column >li.ant-picker-time-panel-cell .ant-picker-time-panel-cell-inner {
+  &:hover {
+    background-color: var(--color-orange-6);
+  }
+}
+.ant-select:not(.ant-select-disabled):not(.ant-select-customize-input):not(.ant-pagination-size-changer):hover .ant-select-selector {
+  border-color: var(--color-theme);
+}
+.ant-select-dropdown .ant-select-item-option-active:not(.ant-select-item-option-disabled) {
+  background-color: var(--color-orange-6);
+}
+.ant-select-dropdown .ant-select-item-option-selected:not(.ant-select-item-option-disabled) {
+  background-color: var(--color-orange-6);
+  div {
+    color: var(--color-theme);
+    font-weight: 400;
+  }
+}
+
+div.ant-select-dropdown {
+  background-color: var(--management-bg-color);
+}
+.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);
+}
+
+
+ .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;
+  }
+ }

+ 16 - 6
src/styles/elementui.scss

@@ -399,6 +399,7 @@ div.el-drawer {
 div .el-input__inner {
   color: var(--color-neutral-1);
   font-size: var(--font-size-3);
+  height: 32px;
 }
 .el-input__inner::placeholder {
   color: var(--color-neutral-3);
@@ -465,7 +466,7 @@ div .el-checkbox--large .el-checkbox__label {
   padding-left: 4px;
 }
 span.el-checkbox__input.is-checked + .el-checkbox__label {
-  color: var(--color-neutral-2);
+  color: var(--color-neutral-1);
 }
 /* 修改选中时打勾图标的大小 */
 div .el-checkbox.el-checkbox--large span.el-checkbox__inner::after {
@@ -579,6 +580,9 @@ div .el-badge__content--warning {
 div .el-badge {
   margin: 8px 0 0 8px;
 }
+.el-date-table td.next-month .el-date-table-cell__text, .el-date-table td.prev-month .el-date-table-cell__text{
+  color: var(--color-el-date-prev);
+}
 
 .el-date-table td.current:not(.disabled) span.el-date-table-cell__text {
   background-color: var(--color-theme);
@@ -634,12 +638,12 @@ div .el-range-editor.is-active,
 .el-range-editor.is-active:hover,
 .el-date-editor.el-input__wrapper:hover,
 .el-date-editor.el-input__wrapper:hover {
-  box-shadow: 0 0 0 1px var(--color-theme) !important;
+  box-shadow: 0 0 0 1px var(--color-theme) inset !important;
   border-color: var(--color-theme);
 }
 
-table.el-date-table td.available:hover {
-  span {
+table.el-date-table td.available:not(.start-date):not(.end-date):hover {
+   span {
     color: var(--color-theme);
   }
 }
@@ -660,11 +664,11 @@ div .el-date-table td.end-date .el-date-table-cell__text,
   color: #ffffff;
 }
 div .el-date-table td.available:hover {
-  color: var(--color-theme);
+  color: var(--color-neutral-1);
 }
 div .el-date-editor .el-range-input,
 div .el-date-editor .el-range-separator {
-  color: var(--color-btn-default-dark-bg);
+  // color: var(--color-btn-default-dark-bg);
   font-size: var(--font-size-3);
 }
 div .el-range-editor.el-input__wrapper {
@@ -909,4 +913,10 @@ div .suggestion-item:hover {
   &:hover {
     background-color: var(--color-arrow-hoverL);
   }
+}
+div .manage-footer-class {
+  box-shadow: 0 -2px 6px rgba(0, 0, 0, 0.15) !important;
+}
+div .schedule-popper {
+  max-width: 328px;
 }

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

@@ -1,9 +1,9 @@
 @font-face {
   font-family: "font_family"; /* Project id 4672385 */
-  src: url('iconfont.woff2?t=1750149505564') format('woff2'),
-       url('iconfont.woff?t=1750149505564') format('woff'),
-       url('iconfont.ttf?t=1750149505564') format('truetype'),
-       url('iconfont.svg?t=1750149505564#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,230 @@
   -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";
+}
+
+.icon-icon_video_b:before {
+  content: "\e743";
+}
+
+.icon-icon_filters_up_b:before {
+  content: "\e742";
+}
+
+.icon-icon_charge_trucking:before {
+  content: "\e741";
+}
+
+.icon-icon_charge_hubrouting:before {
+  content: "\e73a";
+}
+
+.icon-icon_charge_gatewayhandling:before {
+  content: "\e73b";
+}
+
+.icon-icon_charge_airfreight:before {
+  content: "\e73c";
+}
+
+.icon-icon_charge_localhandling:before {
+  content: "\e73d";
+}
+
+.icon-icon_charge_customs:before {
+  content: "\e73e";
+}
+
+.icon-icon_charge_terminalcharge:before {
+  content: "\e73f";
+}
+
+.icon-icon_charge_airfreightrouting:before {
+  content: "\e740";
+}
+
+.icon-icon_barcode_b:before {
+  content: "\e737";
+}
+
+.icon-icon_booking__fill_b1:before {
+  content: "\e738";
+}
+
+.icon-icon_customer_b:before {
+  content: "\e739";
+}
+
+.icon-icon_warning_b:before {
+  content: "\e72d";
+}
+
+.icon-icon_update_b:before {
+  content: "\e72e";
+}
+
+.icon-icon_purchase__order_b:before {
+  content: "\e72f";
+}
+
+.icon-icon_unshare_b:before {
+  content: "\e730";
+}
+
+.icon-icon_organization_b:before {
+  content: "\e731";
+}
+
+.icon-icon_sortingby_b:before {
+  content: "\e732";
+}
+
+.icon-icon_sales__order_b:before {
+  content: "\e733";
+}
+
+.icon-icon_sendtest_b:before {
+  content: "\e734";
+}
+
+.icon-icon_time_full_b:before {
+  content: "\e735";
+}
+
+.icon-icon_datasource_b:before {
+  content: "\e736";
+}
+
+.icon-icon_unpack_b:before {
+  content: "\e72c";
+}
+
+.icon-icon_vgm_n_b:before {
+  content: "\e72b";
+}
+
+.icon-icon_booking_resend:before {
+  content: "\e72a";
+}
+
+.icon-icon_hbl_reopen:before {
+  content: "\e728";
+}
+
+.icon-icon_eci_st_retrigger:before {
+  content: "\e729";
+}
+
+.icon-icon_next_b1:before {
+  content: "\e727";
+}
+
+.icon-icon_default_screen_b:before {
+  content: "\e726";
+}
+
+.icon-icon_update__detail_b:before {
+  content: "\e725";
+}
+
+.icon-icon_brno_b:before {
+  content: "\e71d";
+}
+
+.icon-icon_smart_b:before {
+  content: "\e71e";
+}
+
+.icon-icon_map_b:before {
+  content: "\e71f";
+}
+
+.icon-icon_template_b1:before {
+  content: "\e720";
+}
+
+.icon-icon_door_b:before {
+  content: "\e721";
+}
+
+.icon-icon_map_point_b:before {
+  content: "\e722";
+}
+
+.icon-icon_current__location_b:before {
+  content: "\e723";
+}
+
+.icon-icon_address_book_b:before {
+  content: "\e724";
+}
+
+.icon-icon_guideright_b:before {
+  content: "\e71b";
+}
+
+.icon-icon_guideleft_b:before {
+  content: "\e71c";
+}
+
 .icon-icon_guidelines_b:before {
   content: "\e719";
 }

Diferenças do arquivo suprimidas por serem muito extensas
+ 0 - 0
src/styles/icons/iconfont.js


Diferenças do arquivo suprimidas por serem muito extensas
+ 2 - 0
src/styles/icons/iconfont.svg


BIN
src/styles/icons/iconfont.ttf


BIN
src/styles/icons/iconfont.woff


BIN
src/styles/icons/iconfont.woff2


+ 4 - 0
src/styles/theme-g.scss

@@ -96,4 +96,8 @@
   --color-prompt-diaolog-bg: #3A4149;
   --color-prompt-disabled-bg: rgba(244, 244, 244, 0.20);
   --color-prompt-disabled-border: rgba(101, 111, 125, 0.30);
+
+  // report
+  --color-schedule-bg: #343A43;
+  --color-schedule-details-bg : #343A43;
 }

+ 86 - 3
src/styles/theme.scss

@@ -254,7 +254,9 @@
   .el-input {
     --el-border: #eaebed;
   }
-
+ .el-date-editor {
+    --el-input-border-color: #eaebed;
+  }
   --color-vxe-table-visited-row-bg: #f2f2f2;
 
   --color-public-tracking-empty-bg: #fff;
@@ -266,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;
@@ -354,6 +354,48 @@
   --color-ant-picker-th: #b5b9bf;
 
   --color-json-item-hover: #e6f7ff;
+
+  // report
+  --color-schedule-bg: #F6F8FA;
+  --color-schedule-details-bg: #F5F7FA;
+  
+  --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-booking-tag-item-bg: rgba(130, 0, 218, 0.10);
+  --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 {
@@ -481,6 +523,9 @@
   .el-input {
     --el-border: #656f7d;
   }
+  .el-date-editor {
+    --el-input-border-color: #656f7d;
+  }
   .el-radio {
     --el-radio-input-border: #656f7d;
   }
@@ -581,5 +626,43 @@
   --color-ant-picker-th: rgba(240, 241, 243,0.3);
 
   --color-json-item-hover: #3e5966;
+
+  --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-booking-tag-item-bg: rgba(153, 1, 255, 0.20);
+  --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;
 }
   

+ 4 - 1
src/styles/vxeTable.scss

@@ -48,9 +48,12 @@
 .vxe-table--render-default.vxe-editable .vxe-body--column {
   height: 40px !important;
 }
-
+div .vxe-grid .vxe-grid--table-container {
+  height: 100%;
+}
 div.vxe-table--render-default {
   color: var(--color-neutral-1);
+  height: 100%;
 }
 
 // 需要在表格配置中加上 round: true

+ 43 - 19
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
@@ -63,7 +79,7 @@ class HttpAxios {
           grouping: true
         })
         emitter.emit('login-out');
-      } else if (response.data.code !== 200 && response.data.code !== 400) {
+      } else if (response.data.code !== 200 && response.data.code !== 400 && response.data.code !== 4001 && response.data.code !== 4002 && response.data.code !== 4003) {
         ElMessageBox.alert(
           response.data?.data?.msg || 'The request failed. Please try again later',
           'Prompt',
@@ -76,24 +92,32 @@ class HttpAxios {
   }
 
   _checkResponseError = (error: any) => {
+    // ✅ 第一步:如果是用户取消的请求,静默处理 or 特殊处理
+    if (axios.isCancel(error)) {
+      return Promise.reject(error) // 通常仍 reject,但上层可选择忽略
+    }
+
+    // ✅ 第二步:超时错误(ECONNABORTED)
     if (error.code === 'ECONNABORTED') {
       ElMessage.error({
-        message: 'Request timed out, please try again later!!',
+        message: 'Request timed out, please try again later!',
         grouping: true
       })
       return Promise.reject(error)
     }
 
+    // ✅ 第三步:其他真实错误
     const status = error.response?.status
     const statusText = error.response?.statusText
     const message = error.message
+
     ElMessage.error({
       message: CODE_MESSAGE[status] || statusText || message,
       grouping: true
     })
+
     return Promise.reject(error)
   }
-
   sendRequest = (url: string, params: any, method = 'post', config?: AxiosRequestConfig) => {
     if (!this.instance) return
 

+ 42 - 39
src/utils/table.ts

@@ -6,53 +6,56 @@ import { type VxeGridInstance, type VxeGridProps } from 'vxe-table'
  * @param grid 表格实例
  * @returns
  */
-export const autoWidth = (tableData: VxeGridProps, grid: VxeGridInstance) => {
+export const autoWidth = (tableData: VxeGridProps, grid: VxeGridInstance, customizeWidth?: { [key: string]: number }) => {
   const columns = tableData.columns
   const data = tableData.data
   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 || column.width) 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
-        }
+        width = curStr.length * 11 + 20
       }
-      // 宽度不能小于100
-      // width < 100 && (width = 100)
-      // 最终宽度不能超过400
-      width > 400 && (width = 400)
-      // 如果字段是Mode,则固定宽度为80
-      if (field === 'Mode') {
-        width = 80
-      }
-
-      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

+ 1 - 1
src/views/AIApiLog/src/components/LogDialog.vue

@@ -91,4 +91,4 @@ defineExpose({
     background-color: var(--color-json-item-hover);
   }
 }
-</style>
+</style>

+ 1 - 2
src/views/AIRobotChat/src/AIRobotChat.vue

@@ -335,7 +335,7 @@ const handleLiabilityExeDialog = () => {
 }
 
 const handelclckaiinit = () => {
-  AIQuestion.value.AIRobotInit() 
+  AIQuestion.value.AIRobotInit()
 }
 
 defineExpose({
@@ -344,7 +344,6 @@ defineExpose({
   clearData,
   handelclckaiinit
 })
-
 </script>
 
 <template>

+ 146 - 365
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'
@@ -7,329 +6,98 @@ import DateRange from '@/components/DateRange'
 import MoreFilters from '@/components/MoreFilters'
 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 filterRef: Ref<HTMLElement | null> = ref(null)
+const userStore = useUserStore()
+const formatDate = userStore.dateFormat
+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 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 textSearch: any = ref(
+  filtersList.value.find((item) => item.key === '_textSearch')?.value || ''
+)
 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>
-})
-onMounted(() => {})
-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 = (val: any, value: any, data: any) => {
-  filterData.daterangeData = []
-  if (Object.keys(val).length) {
-    for (const key in val) {
-      let str = `${key}:${val[key]}`
-      filterData.daterangeData.push(str)
-    }
-  }
-  if (sessionStorage.getItem('searchTableQeury') == null) {
-    for (const key in value) {
-      searchTableQeury.f_etd_start = value[key].data[0]
-      searchTableQeury.f_etd_end = value[key].data[1]
-    }
-  } else {
-    searchTableQeury = data
-    if (searchTableQeury._textSearch) {
-      BookingSearch.value = searchTableQeury._textSearch
-    }
-  }
-  getbookingdata()
-  renderTagsData()
-}
-//DateRangeSearch
-const DateRangeSearch = (val: any, value: any) => {
-  filterData.daterangeData = []
-  if (Object.keys(val).length) {
-    for (const key in val) {
-      let str = `${key}:${val[key]}`
-      filterData.daterangeData.push(str)
-    }
   }
-  if (Object.keys(value).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
-  }
-  for (const key in value) {
-    if (key == 'ETD') {
-      searchTableQeury.f_etd_start = value[key].data[0]
-      searchTableQeury.f_etd_end = value[key].data[1]
-    } else if (key == 'ETA') {
-      searchTableQeury.m_eta_start = value[key].data[0]
-      searchTableQeury.m_eta_end = value[key].data[1]
-    } else {
-      searchTableQeury.created_time_start = value[key].data[0]
-      searchTableQeury.created_time_end = value[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()
+
+initPage()
+
+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 getbookingdata = () => {
+const getBookingData = () => {
+  const queryData = filtersStore.getQueryData
+
   tableLoadingTableData.value = true
   BookingTable_ref.value.getLoadingData(tableLoadingTableData.value)
   $api
@@ -338,29 +106,28 @@ const getbookingdata = () => {
       ps: BookingTable_ref.value.pageInfo.pageSize,
       rc: -1,
       other_filed: '',
-      ...searchTableQeury
+      _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
           }
@@ -370,25 +137,33 @@ 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'
 
@@ -397,6 +172,19 @@ const bookingGuideRef = ref()
 const handleGuide = () => {
   bookingGuideRef.value.startGuide() // 开始引导
 }
+
+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>
@@ -408,12 +196,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"
             >
@@ -443,27 +231,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"
@@ -471,34 +250,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>
 

+ 2 - 0
src/views/Booking/src/components/BookingDetail/src/BookingDetail.vue

@@ -374,6 +374,8 @@ const { isOverflow: isDestinationOverflow } = useOverflow(destinationRef, allDat
     color: var(--color-neutral-2);
   }
   .content {
+    width: 100%;
+    word-wrap: break-word;
     min-height: 16px;
     font-size: 14px;
     font-weight: 700;

+ 12 - 2
src/views/Booking/src/components/BookingDetail/src/components/BasicInformation.vue

@@ -160,7 +160,8 @@ const convertData = (data: any) => {
           label: 'Service Type',
           content: data.basicInfo['Service_Type'] || '--'
         }
-      ]
+      ],
+      bol_type: data.basicInfo.bol_type || '--'
     },
     businessPartners: [
       {
@@ -320,7 +321,16 @@ defineExpose({
             Add Reference
           </el-button> -->
         </div>
-        <div class="content" v-if="item.label !== 'Ref No.' && item.type !== 'link'">
+
+        <div
+          class="content"
+          v-if="
+            (item.label !== 'Ref No.' && item.type !== 'link') ||
+            (item.type === 'link' &&
+              (allData?.basicInformation?.bol_type?.toLowerCase() === 'booking' ||
+                item.content === '--'))
+          "
+        >
           {{ item.content }}
         </div>
         <div

+ 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 }}

+ 21 - 11
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) => {
@@ -397,7 +403,11 @@ const handleCustomizeColumns = () => {
       model_name: 'Booking_Search'
     }
   }
-  CustomizeColumnsRef.value.openDialog(params, -220)
+  CustomizeColumnsRef.value.openDialog(
+    params,
+    -220,
+    'Drag item over to this selection or click "add" icon to show the column on your booking list'
+  )
 }
 // 定制表格
 const customizeColumns = async () => {
@@ -422,13 +432,13 @@ const handleLinkClick = (row: any, column: any) => {
       path: '/booking/detail',
       query: { a: row.__serial_no, _schemas: row._schemas, status: row.Status }
     })
-    visitedRowState.setBookingTableData(row['__serial_no'])
   } else if (column.title === 'HBOL/HAWB No.') {
     router.push({
       path: '/tracking/detail',
       query: { a: row.__serial_no, _schemas: row._schemas }
     })
   }
+  visitedRowState.setBookingTableData(row['__serial_no'])
 }
 
 const selectedNumber = ref(0)

+ 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">

Diferenças do arquivo suprimidas por serem muito extensas
+ 419 - 427
src/views/Dashboard/src/DashboardView.vue


+ 238 - 198
src/views/Dashboard/src/components/BarChart.vue

@@ -2,230 +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
-})
-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
-    initChart()
+  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 || 'Total:', //主标题
-    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: themeStore.theme === 'dark' ? '#f0f1f3' : '#2B2F36',
+        fontWeight: '700',
+        fontFamily: 'Lato-Light',
+        fontSize: '14px'
       }
     },
-    axisLabel: {
-      interval: 0,
-      fontFamily: 'Lato-Light',
-      color: '#B5B9BF'
-    }
-  },
-  yAxis: {
-    type: 'value',
-    scale: true,
-    splitLine: {
+    grid: {
+      top: '18%',
+      left: '1%',
+      right: '2%',
+      bottom: '5%',
+      containLabel: true
+    },
+    tooltip: {
       show: true,
-      lineStyle: {
-        type: 'dashed',
-        color: '#eaebed'
+      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'
       }
     },
-    axisLine: {
-      show: true,
-      lineStyle: {
-        color: '#eaebed'
+    xAxis: {
+      type: 'category',
+      data: barName.value,
+      axisTick: { show: false },
+      axisLine: {
+        lineStyle: {
+          color: themeStore.theme === 'dark' ? '#8d9095' : '#eaebed'
+        }
+      },
+      axisLabel: {
+        interval: 0,
+        fontFamily: 'Lato-Light',
+        color: '#B5B9BF'
       }
     },
-    axisLabel: {
-      fontFamily: 'Lato-Light',
-      color: '#B5B9BF',
-      formatter: function (value: any) {
-        return formatNumber(value, 0)
+    yAxis: {
+      type: 'value',
+      scale: true,
+      splitLine: {
+        show: true,
+        lineStyle: {
+          type: 'dashed',
+          color: themeStore.theme === 'dark' ? '#8d9095' : '#eaebed'
+        }
+      },
+      axisLine: {
+        show: true,
+        lineStyle: {
+          color: themeStore.theme === 'dark' ? '#8d9095' : '#eaebed'
+        }
+      },
+      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'
       }
     },
-    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: themeStore.theme === 'dark' ? '#f0f1f3' : '#2B2F36'
+      },
+      emphasis: {
+        iconStyle: {
+          borderColor: '#ff7500'
+        }
+      },
+      feature: {
+        saveAsImage: {
+          icon: 'path://M588.8 830.976H179.52a48 48 0 0 1-48-48v-110.72l212.032-120.96 117.76 67.2a16 16 0 0 0 15.744 0.128l396.16-220.8-0.192-0.256h0.384v-156.8c0-44.16-35.84-80-80-80H179.52c-44.16 0-80 35.84-80 80v542.208c0 44.16 35.84 80 80 80H588.8v-32zM131.52 240.832a48 48 0 0 1 48-48h613.888a48 48 0 0 1 48 48V378.88L469.312 586.24l-117.76-67.2a16.128 16.128 0 0 0-12.032-1.664l-3.84 1.6-204.16 116.48V240.896zM309.632 467.2a85.76 85.76 0 0 0 85.376-76.992l0.384-8.768c0-44.416-33.728-80.96-76.992-85.376L309.76 295.68c-47.36 0-85.824 38.4-85.824 85.76l0.448 8.832A85.76 85.76 0 0 0 300.8 466.816l8.832 0.448z m0-32a53.76 53.76 0 1 1 0-107.584 53.76 53.76 0 0 1 0 107.52z m452.736 423.04a16 16 0 0 0 22.592 0l138.368-138.368-22.592-22.656-111.104 111.104V545.6h-32v262.784l-111.04-111.104-22.592 22.656 138.368 138.368z',
+          show: true,
+          name: bar_title.value || props.saveImageName || 'data',
+          type: 'png',
+          backgroundColor: '#fff'
+        }
+      },
+      showTitle: false
+    },
+    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, 强制重置系列数据,防止旧数据残留
     }
   },
-  series: bar_series.value
-})
+  { 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 }
+        }
+      }
+    })
+  },
+  { immediate: true, deep: true }
+)
+
+// 监听窗口大小变化
+const handleResize = () => {
+  if (chartInstance) {
+    chartInstance.resize()
+  }
+}
+
 onMounted(() => {
   initChart()
-  clickParams()
-  watch(
-    () => themeStore.theme,
-    (newVal) => {
-      if (newVal === 'dark') {
-        initOption.xAxis.axisLine.lineStyle.color = '#3F434A'
-        initOption.yAxis.axisLine.lineStyle.color = '#3F434A'
-        initOption.yAxis.splitLine.lineStyle.color = '#3F434A'
-        initChart()
-      } else {
-        initOption.xAxis.axisLine.lineStyle.color = '#eaebed'
-        initOption.yAxis.axisLine.lineStyle.color = '#eaebed'
-        initOption.yAxis.splitLine.lineStyle.color = '#eaebed'
-        initChart()
-      }
-    },
-    {
-      immediate: true,
-      deep: true
-    }
-  )
+  window.addEventListener('resize', handleResize)
+})
+
+onBeforeUnmount(() => {
+  // 清理资源
+  window.removeEventListener('resize', handleResize)
+  if (chartInstance) {
+    chartInstance.dispose()
+    chartInstance = null
+  }
 })
 
+// 暴露数据给父组件
 const emits = defineEmits(['ClickParams'])
 const paramsdata = ref({
   name: '',
   type: ''
 })
-const clickParams = () => {
-  const bar_chart = echarts.init(bar_ref.value)
-  // 监听点击事件
-  bar_chart.on('click', function (params) {
-    paramsdata.value.name = params.name
-    paramsdata.value.type = params.seriesName
-    emits('ClickParams')
-  })
-}
-const initChart = () => {
-  const bar_chart = echarts.init(bar_ref.value)
-  //图表响应式
-  window.addEventListener('resize', () => {
-    bar_chart.resize()
-  })
-  bar_chart.setOption(initOption)
-}
 
 defineExpose({
   paramsdata
@@ -234,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>
 
@@ -245,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>

+ 106 - 92
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'])
+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
+    if (props.isPending) {
+      DashDate.value = ['', '']
     } 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
-    } 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'
@@ -206,14 +207,19 @@ const guideStore = useGuideStore()
         </el-button>
       </template>
       <div class="Dash_title">Transport Mode</div>
-      <div class="filter_filter">
+      <div class="filter_filter_one">
         <el-checkbox-group
           @change="changeCheckboxGroup1"
           v-model="checkboxGroup1"
           size="large"
           :disabled="checkboxDisabled"
         >
-          <el-checkbox-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>
@@ -373,9 +375,21 @@ const guideStore = useGuideStore()
   justify-content: center;
   margin: 8px 16px 0 16px;
 }
+.filter_filter_one {
+  margin: 0 16px;
+  :deep(.el-checkbox-group) {
+    display: flex;
+  }
+}
 .filter_filter {
   margin-left: 16px;
 }
+.filter_button {
+  width: 20%;
+  :deep(.el-checkbox-button__inner) {
+    width: 100%;
+  }
+}
 .dash_flex {
   display: flex;
   align-items: center;

+ 80 - 39
src/views/Dashboard/src/components/PieChart.vue

@@ -4,6 +4,7 @@ import * as echarts from 'echarts'
 import { useThemeStore } from '@/stores/modules/theme'
 import { onMounted, ref, reactive, watch, computed } from 'vue'
 import { formatNumber } from '@/utils/tools'
+
 const props = defineProps({
   PieData: Object
 })
@@ -15,7 +16,9 @@ watch(
     pie_data.value = current
     initOption.title.text = pie_title.value
     initOption.toolbox.feature.saveAsImage.name = downloadName.value
-    initChart()
+    nextTick(() => {
+      pieChart.value.setOption(initOption)
+    })
   },
   {
     deep: true
@@ -46,6 +49,25 @@ const downloadName = computed(() => {
 })
 
 const initOption: any = reactive({
+  // graphic: {
+  //   elements: [
+  //     {
+  //       type: 'text',
+  //       right: '16',
+  //       bottom: '16',
+  //       style: {
+  //         text: 'Total: $12,500',
+  //         fontSize: 14,
+  //         fill: '#666'
+  //       },
+  //       // 直接设置 onclick 回调
+  //       onclick: function () {
+  //         alert('Total clicked!')
+  //         // 或者执行其他逻辑
+  //       }
+  //     }
+  //   ]
+  // },
   //标题
   title: {
     text: pie_title.value || '', //主标题
@@ -90,6 +112,8 @@ const initOption: any = reactive({
     }
   },
   toolbox: {
+    top: 6,
+    right: 8,
     iconStyle: {
       borderColor: '#2B2F36'
     },
@@ -98,10 +122,16 @@ const initOption: any = reactive({
         borderColor: '#ff7500'
       } // hover上去时的颜色
     },
-    show: false, // 显示工具箱
+    show: true, // 显示工具箱
     feature: {
-      restore: { show: true },
-      saveAsImage: { show: true, name: downloadName.value }
+      // restore: { show: true },
+      saveAsImage: {
+        icon: 'path://M588.8 830.976H179.52a48 48 0 0 1-48-48v-110.72l212.032-120.96 117.76 67.2a16 16 0 0 0 15.744 0.128l396.16-220.8-0.192-0.256h0.384v-156.8c0-44.16-35.84-80-80-80H179.52c-44.16 0-80 35.84-80 80v542.208c0 44.16 35.84 80 80 80H588.8v-32zM131.52 240.832a48 48 0 0 1 48-48h613.888a48 48 0 0 1 48 48V378.88L469.312 586.24l-117.76-67.2a16.128 16.128 0 0 0-12.032-1.664l-3.84 1.6-204.16 116.48V240.896zM309.632 467.2a85.76 85.76 0 0 0 85.376-76.992l0.384-8.768c0-44.416-33.728-80.96-76.992-85.376L309.76 295.68c-47.36 0-85.824 38.4-85.824 85.76l0.448 8.832A85.76 85.76 0 0 0 300.8 466.816l8.832 0.448z m0-32a53.76 53.76 0 1 1 0-107.584 53.76 53.76 0 0 1 0 107.52z m452.736 423.04a16 16 0 0 0 22.592 0l138.368-138.368-22.592-22.656-111.104 111.104V545.6h-32v262.784l-111.04-111.104-22.592 22.656 138.368 138.368z',
+        show: true,
+        name: downloadName.value,
+        type: 'png',
+        backgroundColor: '#fff'
+      }
     },
     showTitle: false
   },
@@ -143,15 +173,13 @@ const initOption: any = reactive({
       length2: 0,
       maxSurfaceAngle: 80
     },
-    labelLayout: function (params: any) {
-      const pie_chart = echarts.init(pie_ref.value)
-      const isLeft = params.labelRect.x < pie_chart.getWidth() / 2
+    labelLayout: (params) => {
+      const isLeft = params.labelRect.x < containerWidth.value / 2
       const points = params.labelLinePoints
-      // Update the end point.
-      points[2][0] = isLeft ? params.labelRect.x : params.labelRect.x + params.labelRect.width
-      return {
-        labelLinePoints: points
+      if (points?.[2]) {
+        points[2][0] = isLeft ? params.labelRect.x : params.labelRect.x + params.labelRect.width
       }
+      return { labelLinePoints: points }
     }
   },
   legend: {
@@ -168,49 +196,62 @@ const initOption: any = reactive({
     }
   }
 })
-
+const containerWidth = ref(0)
+onMounted(() => {
+  if (pie_ref.value) {
+    containerWidth.value = pie_ref.value.clientWidth
+    // 如果需要响应式 resize,可用 ResizeObserver
+  }
+})
 onMounted(() => {
   initChart()
   clickParams()
-  watch(
-    () => themeStore.theme,
-    (newVal) => {
-      if (newVal === 'dark') {
-        initOption.title.textStyle.color = '#f0f1f3'
-        initOption.series.label.rich.time.color = 'rgba(240,241,243,0.7)'
-        initOption.series.label.rich.name.color = 'rgba(240,241,243,0.7)'
-        initOption.legend.textStyle.color = 'rgba(240,241,243,0.7)'
-        initChart()
-      } else {
-        initOption.title.textStyle.color = '#2B2F36'
-        initOption.series.label.rich.time.color = '#999'
-        initOption.series.label.rich.name.color = '#646A73'
-        initOption.legend.textStyle.color = '#646A73'
-        initChart()
-      }
-    },
-    {
-      immediate: true,
-      deep: true
-    }
-  )
 })
+watch(
+  () => themeStore.theme,
+  (newVal) => {
+    if (newVal === 'dark') {
+      initOption.title.textStyle.color = '#f0f1f3'
+      initOption.series.label.rich.time.color = 'rgba(240,241,243,0.7)'
+      initOption.series.label.rich.name.color = 'rgba(240,241,243,0.7)'
+      initOption.legend.textStyle.color = 'rgba(240,241,243,0.7)'
+      initOption.toolbox.iconStyle.borderColor = '#f0f1f3'
+      initOption.toolbox.feature.saveAsImage.backgroundColor = '#2B2F36'
+      nextTick(() => {
+        pieChart.value.setOption(initOption)
+      })
+    } else {
+      initOption.title.textStyle.color = '#2B2F36'
+      initOption.series.label.rich.time.color = '#999'
+      initOption.series.label.rich.name.color = '#646A73'
+      initOption.legend.textStyle.color = '#646A73'
+      initOption.toolbox.iconStyle.borderColor = '#2B2F36'
+      initOption.toolbox.feature.saveAsImage.backgroundColor = '#fff'
+      nextTick(() => {
+        pieChart.value.setOption(initOption)
+      })
+    }
+  },
+  {
+    immediate: true,
+    deep: true
+  }
+)
 const emits = defineEmits(['ClickParams'])
 const paramsdata = ref()
+const pieChart = ref()
 const clickParams = () => {
-  const pie_chart = echarts.init(pie_ref.value)
   // 监听点击事件
-  pie_chart.on('click', function (params) {
+  pieChart.value.on('click', function (params) {
     paramsdata.value = params.data
     emits('ClickParams')
   })
 }
 const initChart = () => {
-  const pie_chart = echarts.init(pie_ref.value)
-  pie_chart.setOption(initOption)
+  pieChart.value = echarts.init(pie_ref.value)
   //图表响应式
   window.addEventListener('resize', () => {
-    pie_chart.resize()
+    pieChart.value.resize()
   })
 }
 defineExpose({

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

@@ -45,7 +45,6 @@ const SubscribeShipments = (val: any) => {
     })
     .then((res: any) => {
       if (res.code === 200) {
-        console.log(res.data)
       }
     })
 }
@@ -85,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' : ''"
         >

+ 79 - 23
src/views/Dashboard/src/components/RevenueChart.vue

@@ -3,9 +3,9 @@
 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'
+import { on } from 'events'
 
 const themeStore = useThemeStore()
 const props = defineProps({
@@ -23,9 +23,10 @@ watch(
     initOption.xAxis.data = barName.value
     initOption.series = bar_series.value
     initOption.legend.data = Name.value
-    initOption.toolbox.feature.saveAsImage.name = downloadName.value
     initOption.toolbox.show = isShowTooltips.value
-    initChart()
+    nextTick(() => {
+      revenueChart.value.setOption(initOption)
+    })
   },
   {
     deep: true
@@ -37,7 +38,9 @@ watch(
     bar_data.value = current
     initOption.yAxis.max = Max.value
     initOption.yAxis.interval = interval.value
-    initChart()
+    nextTick(() => {
+      revenueChart.value.setOption(initOption)
+    })
   },
   {
     deep: true,
@@ -74,12 +77,14 @@ const Name = computed(() => {
     return []
   }
 })
-const downloadName = computed(() => {
-  return bar_data.value?.download_name
-})
+
 const isShowTooltips = computed(() => {
   return bar_data.value?.isShowTooltips
 })
+
+const downloadName = computed(() => {
+  return bar_data.value?.download_name
+})
 const exportData = ({ filename }: any) => {
   $api
     .RevenueDownload({
@@ -117,8 +122,8 @@ const exportData = ({ filename }: any) => {
             sheetName: res.data.r3_title
           }
         ]
-        let formatJson = (filterVal: any, jsonData: any) => {
-          return jsonData.map((v: any) => filterVal.map((j: any) => v[j]))
+        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)
@@ -127,7 +132,7 @@ const exportData = ({ filename }: any) => {
         }
         // 将表头插入数据数组中
         for (let i = 0; i < header.length; i++) {
-          data[i].unshift(header[i])
+          data[i]?.unshift(header[i])
         }
         let ws_name = sheetname
         // 创建工作簿对象
@@ -284,16 +289,28 @@ const initOption = reactive({
         borderColor: '#ff7500'
       } // hover上去时的颜色
     },
-    itemSize: 25,
     show: isShowTooltips.value, // 显示工具箱
+    top: 6,
+    right: 8,
     feature: {
-      saveAsImage: { show: false, name: downloadName.value },
+      saveAsImage: {
+        icon: 'path://M588.8 830.976H179.52a48 48 0 0 1-48-48v-110.72l212.032-120.96 117.76 67.2a16 16 0 0 0 15.744 0.128l396.16-220.8-0.192-0.256h0.384v-156.8c0-44.16-35.84-80-80-80H179.52c-44.16 0-80 35.84-80 80v542.208c0 44.16 35.84 80 80 80H588.8v-32zM131.52 240.832a48 48 0 0 1 48-48h613.888a48 48 0 0 1 48 48V378.88L469.312 586.24l-117.76-67.2a16.128 16.128 0 0 0-12.032-1.664l-3.84 1.6-204.16 116.48V240.896zM309.632 467.2a85.76 85.76 0 0 0 85.376-76.992l0.384-8.768c0-44.416-33.728-80.96-76.992-85.376L309.76 295.68c-47.36 0-85.824 38.4-85.824 85.76l0.448 8.832A85.76 85.76 0 0 0 300.8 466.816l8.832 0.448z m0-32a53.76 53.76 0 1 1 0-107.584 53.76 53.76 0 0 1 0 107.52z m452.736 423.04a16 16 0 0 0 22.592 0l138.368-138.368-22.592-22.656-111.104 111.104V545.6h-32v262.784l-111.04-111.104-22.592 22.656 138.368 138.368z',
+        show: true,
+        name: 'Revenue Spent',
+        pixelRatio: 2
+      },
       myTool1: {
         show: true,
         title: 'update',
-        icon: 'image://' + updateIcon,
+        icon:
+          'path://' +
+          'M560.384 115.392a16.064 16.064 0 0 1 13.056 4.608l257.216 257.216c1.92 1.92 3.136 4.288 3.84 6.784h0.832v118.72h-32V404.48H626.112c-44.16 0-80-35.84-80-80V147.2H230.208a48 48 0 0 0-48 48v672c0 26.56 21.504 48 48 48h341.504v32H230.208c-44.16 0-80-35.776-80-80V195.2c0-44.16 35.84-80 80-80h330.24v0.192z m232.768 495.296v281.984l119.872-119.872 22.592 22.656-147.072 147.2a16.064 16.064 0 0 1-22.656 0l-147.2-147.2 22.656-22.656 119.808 119.872V610.688h32zM427.648 476.352c2.496 0 4.224 0.448 5.312 1.216a8.96 8.96 0 0 1 3.072 3.84L474.24 565.12a48.256 48.256 0 0 1 1.792-3.2c0.64-1.152 1.344-2.24 2.112-3.392l53.952-76.736a13.76 13.76 0 0 1 3.648-4.032 9.152 9.152 0 0 1 4.864-1.28h37.76L498.432 583.04l59.712 125.824h-32.64a7.872 7.872 0 0 1-5.76-2.112 15.936 15.936 0 0 1-3.264-4.48l-43.52-97.088a42.112 42.112 0 0 1-3.2 5.376l-64.768 91.648a14.208 14.208 0 0 1-5.312 5.248 13.952 13.952 0 0 1-6.272 1.408h-36.352L448 584.768l-53.248-108.416h32.96zM578.112 324.48c0 26.496 21.504 48 48 48h154.56L578.176 169.92V324.48z',
         onclick: function () {
-          let filename = 'Revenue Spent Details ' + barName.value[0] + '-' + barName.value[barName.value.length - 1]
+          let filename =
+            'Revenue Spent Details ' +
+            barName.value[0] +
+            '-' +
+            barName.value[barName.value.length - 1]
           exportData({ filename: filename })
         }
       }
@@ -307,19 +324,27 @@ onMounted(() => {
     () => themeStore.theme,
     (newVal) => {
       if (newVal === 'dark') {
-        initOption.title.textStyle.color = '#f0f1f3'
+        initOption.title.textStyle.color = '#fff'
         initOption.xAxis.axisLine.lineStyle.color = '#3F434A'
         initOption.xAxis.axisLine.lineStyle.color = '#3F434A'
         initOption.yAxis.axisLine.lineStyle.color = '#3F434A'
         initOption.legend.textStyle.color = 'rgba(240,241,243,0.7)'
-        initChart()
+        initOption.toolbox.iconStyle.borderColor = '#f0f1f3'
+        initOption.toolbox.feature.saveAsImage.backgroundColor = '#3F434A'
+        nextTick(() => {
+          revenueChart.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.legend.textStyle.color = '#646A73'
-        initChart()
+        initOption.toolbox.iconStyle.borderColor = '#2B2F36'
+        initOption.toolbox.feature.saveAsImage.backgroundColor = '#fff'
+        nextTick(() => {
+          revenueChart.value.setOption(initOption)
+        })
       }
     },
     {
@@ -329,13 +354,14 @@ onMounted(() => {
   )
 })
 
+const revenueChart = ref()
 const initChart = () => {
-  const bar_chart = echarts.init(bar_ref.value)
+  revenueChart.value = markRaw(echarts.init(bar_ref.value))
   //图表响应式
   window.addEventListener('resize', () => {
-    bar_chart.resize()
+    revenueChart.value.resize()
   })
-  bar_chart.on('legendselectchanged', function (event: any) {
+  revenueChart.value.on('legendselectchanged', function (event: any) {
     const selected = event.selected
     let trueCount = 0
     let trueObj: any = {}
@@ -380,14 +406,20 @@ const initChart = () => {
       initOption.yAxis.max = max
       initOption.yAxis.interval = interval
     }
-    bar_chart.setOption(initOption)
+    revenueChart.value.setOption(initOption)
   })
-  bar_chart.setOption(initOption)
 }
+
+onUnmounted(() => {
+  window.removeEventListener('resize', () => {
+    revenueChart.value.resize()
+  })
+  revenueChart.value?.dispose()
+})
 </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>
@@ -398,6 +430,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%;

+ 259 - 195
src/views/Dashboard/src/components/SellerChart.vue

@@ -1,228 +1,290 @@
-<!-- 横形柱状图 -->
 <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
+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
-    initChart()
-  },
-  {
-    deep: true
-  }
-)
-watch(
-  () => props.Interval,
-  (current) => {
-    seller_interval.value = current
-    initOption.xAxis.max = Max.value
-    initOption.xAxis.interval = interval.value
-    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: '5%',
-    left: '3%',
-    right: '6%',
-    bottom: '5%',
-    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' : '#2B2F36'
+  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 830.976H179.52a48 48 0 0 1-48-48v-110.72l212.032-120.96 117.76 67.2a16 16 0 0 0 15.744 0.128l396.16-220.8-0.192-0.256h0.384v-156.8c0-44.16-35.84-80-80-80H179.52c-44.16 0-80 35.84-80 80v542.208c0 44.16 35.84 80 80 80H588.8v-32zM131.52 240.832a48 48 0 0 1 48-48h613.888a48 48 0 0 1 48 48V378.88L469.312 586.24l-117.76-67.2a16.128 16.128 0 0 0-12.032-1.664l-3.84 1.6-204.16 116.48V240.896zM309.632 467.2a85.76 85.76 0 0 0 85.376-76.992l0.384-8.768c0-44.416-33.728-80.96-76.992-85.376L309.76 295.68c-47.36 0-85.824 38.4-85.824 85.76l0.448 8.832A85.76 85.76 0 0 0 300.8 466.816l8.832 0.448z m0-32a53.76 53.76 0 1 1 0-107.584 53.76 53.76 0 0 1 0 107.52z m452.736 423.04a16 16 0 0 0 22.592 0l138.368-138.368-22.592-22.656-111.104 111.104V545.6h-32v262.784l-111.04-111.104-22.592 22.656 138.368 138.368z',
+          show: true,
+          name: props.saveImageName,
+          type: 'png',
+          color: themeStore.theme === 'dark' ? '#3F434A' : '#fff',
+          backgroundColor: toolboxBgColor
         }
       }
     }
-  ]
-})
+  }
+
+  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'
-        initChart()
-      } else {
-        initOption.xAxis.splitLine.lineStyle.color = '#eaebed'
-        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>
 
@@ -233,8 +295,10 @@ defineExpose({
   overflow: hidden;
   position: relative;
 }
-#seller_chart {
+
+.seller-chart {
   width: 100%;
-  height: 272px;
+  height: 310px;
+  // 移除 #id 选择器,使用 class 更符合 Vue 规范
 }
 </style>

+ 2 - 2
src/views/Dashboard/src/components/TopMap.vue

@@ -22,7 +22,7 @@ const init = () => {
   }
   map = L.map('map', { attributionControl: false }).setView([51.505, -0.09], 3)
 
-  L.tileLayer('https://map.kerryapex.com/osm_tiles/{z}/{x}/{y}.png', {
+  L.tileLayer('https://map.k-apex.kln.com/osm_tiles/{z}/{x}/{y}.png', {
     attribution:
       '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
   }).addTo(map)
@@ -137,7 +137,7 @@ defineExpose({
 <template>
   <div
     id="map"
-    style="width: 100%; height: 272px"
+    style="width: 100%; height: 310px"
     :class="{ 'dark-mode': themeStore.theme === 'dark' }"
   ></div>
 </template>

BIN
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>

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

@@ -0,0 +1,669 @@
+<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'
+import { debounce } from 'lodash'
+import axios from 'axios'
+
+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 consigneeValue = ref('')
+const consigneeLoading = ref(false)
+const consignessList = ref([])
+
+const getDataByDate = (date: Dayjs, key: string) => {
+  return calendarData.value?.[date.format('YYYY-MM-DD')]?.[key] ?? 0
+}
+const clearConsigneeList = () => {
+  consignessList.value = []
+}
+const remoteMethod = (query: string) => {
+  currentController.value?.abort()
+
+  const newController = new AbortController()
+  currentController.value = newController
+  consigneeLoading.value = true
+
+  $api
+    .getDeliveryCalendarConsignee(
+      {
+        month: dayjs(displayMonth.value).format('MM/YYYY'),
+        consignee: query
+      },
+      { signal: newController.signal }
+    )
+    .then((res) => {
+      if (!newController.signal.aborted && res.code === 200) {
+        consignessList.value = res.data || []
+      }
+    })
+    .catch((err) => {
+      consignessList.value = []
+      if (!axios.isCancel(err) && !newController.signal.aborted) {
+        ElMessage.error('Failed to load options')
+      }
+    })
+    .finally(() => {
+      // 仅当这是最新请求时,才关闭 loading
+      if (currentController.value === newController) {
+        consigneeLoading.value = false
+      }
+    })
+}
+
+const debouncedRemoteMethod = debounce(remoteMethod, 200)
+const handleVisibleChange = (visible) => {
+  !visible && (consignessList.value = [])
+  getPageData()
+}
+
+const currentController = ref<AbortController | null>(null)
+
+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'),
+      consignee: consigneeValue.value
+    })
+    .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'))
+}
+
+const test = () => {
+  ElMessageBox.alert(
+    "This account's password has expired and is currently unavailable. Please select a different customer account to continue.",
+    {
+      confirmButtonText: 'OK',
+      confirmButtonClass: 'el-button--dark'
+    }
+  )
+}
+</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
+            :bordered="false"
+            :value="value.year()"
+            style="width: 140px"
+            @change="(year) => handleYearChange(year, onChange)"
+          >
+            <a-select-option v-for="y in yearOptions" :key="y" :value="y">
+              {{ y }}
+            </a-select-option>
+            <template #suffixIcon>
+              <!-- 这里可以使用任何图标组件,包括 Element Plus 的 -->
+              <span class="font_family icon-icon_dropdown_b"></span>
+            </template>
+          </a-select>
+
+          <a-select
+            :bordered="false"
+            :value="value.month() + 1"
+            style="width: 92px"
+            @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>
+            <template #suffixIcon>
+              <!-- 这里可以使用任何图标组件,包括 Element Plus 的 -->
+              <span class="font_family icon-icon_dropdown_b"></span>
+            </template>
+          </a-select>
+          <div class="grid-lines"></div>
+
+          <el-select
+            v-model="consigneeValue"
+            filterable
+            reserve-keyword
+            clearable
+            placeholder="Consignee"
+            :loading="consigneeLoading"
+            style="width: 240px"
+            popper-class="part-id-select-popper"
+            :filter-method="debouncedRemoteMethod"
+            @change="handleVisibleChange"
+            @blur="clearConsigneeList"
+          >
+            <el-option
+              v-for="item in consignessList"
+              :key="item.value"
+              :label="item.value"
+              :value="item.value"
+            >
+            </el-option>
+          </el-select>
+        </div>
+      </template>
+
+      <template #dateCellRender="{ current }">
+        <ul class="events">
+          <!-- 如果不为当前月份的日期,则不显示标签 -->
+          <!-- v-if="
+              calendarData?.[dayjs(current).format('YYYY-MM-DD')] &&
+              dayjs(current).isSame(dayjs(displayMonth), 'month')
+            " -->
+          <div class="tags-details">
+            <!-- <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
+              v-if="
+                getDataByDate(current, 'bookingNumber') || getDataByDate(current, 'bookingCtns')
+              "
+              class="booking-tag"
+              @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>
+                <div class="grid-lines"></div>
+                <span class="ctns-tag">{{ getDataByDate(current, 'bookingCtns') }} ctns</span>
+              </div>
+              <div class="list">
+                <div class="item" v-for="size in getDataByDate(current, 'ctnrSize')">
+                  {{ size }}
+                </div>
+              </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 {
+  margin-top: -46px;
+  padding: 0 24px;
+}
+
+.custom-header {
+  display: flex;
+  justify-content: flex-end;
+  align-items: center;
+  padding: 2px 0;
+  padding-right: 1px;
+  :deep(.ant-select-selection-item) {
+    margin-right: 4px;
+    font-size: 24px;
+    font-weight: 700;
+    text-align: right;
+  }
+  :deep(
+    .ant-select-focused.ant-select:not(.ant-select-disabled):not(.ant-select-customize-input):not(
+        .ant-pagination-size-changer
+      )
+      div.ant-select-selector
+  ) {
+    border: 1px solid transparent !important;
+  }
+  .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);
+  }
+  .grid-lines {
+    height: 12px;
+    width: 1px;
+    margin-left: 6px;
+    background-color: var(--color-calendar-booking-tag-label-text);
+  }
+  .list {
+    padding: 6px 8px 8px;
+    .item {
+      height: 12px;
+      width: 100%;
+      padding: 2px 4px;
+      margin-top: 1px;
+      font-size: 8px;
+      line-height: 10px;
+      background-color: var(--color-calendar-booking-tag-item-bg);
+      // background-color: red;
+      &:first-child {
+        margin-top: 0;
+        border-radius: 3px 3px 0 0;
+      }
+      &:last-child {
+        border-radius: 0 0 3px 3px;
+      }
+    }
+  }
+}
+.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>

+ 10 - 11
src/views/DestinationDelivery/src/components/ConfiguRations/src/ConfiguRations.vue

@@ -4,7 +4,7 @@ import { useCalculatingHeight } from '@/hooks/calculatingHeight'
 import { useRouter } from 'vue-router'
 
 const filterRef: Ref<HTMLElement | null> = ref(null)
-  const router = useRouter()
+const router = useRouter()
 
 const AddRulesTableColumns = ref([
   {
@@ -20,15 +20,10 @@ const AddRulesTableColumns = ref([
     type: 'normal',
     width: '20%',
     formatter: ''
-  },{
-    field: 'booking_window_desc',
-    title: 'Booking Window',
-    type: 'normal',
-    formatter: ''
   },
   {
-    field: 'recommended_delivery_date_desc',
-    title: 'Reecommended Delivery Date',
+    field: 'booking_window_desc',
+    title: 'Booking Window',
     type: 'normal',
     formatter: ''
   }
@@ -43,7 +38,7 @@ const gettabledatalength = (val: any) => {
 // 跳转Create New Rule页面
 const ToCreateRule = () => {
   router.push({
-    path: '/destination-delivery/ConfiguRations/CreateNewRule',
+    path: '/destination-delivery/Configurations/create-new-rule',
     query: {}
   })
 }
@@ -63,7 +58,11 @@ const ToCreateRule = () => {
           >+ Add Rule</el-button
         >
       </div>
-      <ConfigurationsTable :height="containerHeight" :ColumnsList="AddRulesTableColumns" @gettabledatalength="gettabledatalength"></ConfigurationsTable>
+      <ConfigurationsTable
+        :height="containerHeight"
+        :ColumnsList="AddRulesTableColumns"
+        @gettabledatalength="gettabledatalength"
+      ></ConfigurationsTable>
     </div>
   </div>
 </template>
@@ -92,4 +91,4 @@ const ToCreateRule = () => {
   padding-right: 24px;
   align-items: end;
 }
-</style>
+</style>

+ 22 - 17
src/views/DestinationDelivery/src/components/ConfiguRations/src/components/ConfigurationsTable.vue

@@ -101,27 +101,29 @@ const handleDelete = (row: any) => {
 const deleteMoniTable = (row: any) => {
   row.visible = false
   $api
-  .deleteConfigurationList({
-    a: row._serial_no
-  })
-  .then((res: any) => {
-    if (res.code === 200) {
-      tableData.value.data = tableData.value.data?.filter((item) => item._serial_no !== row._serial_no)
-      emits('gettabledatalength', tableData.value.data?.length)
-    }
-  })
+    .deleteConfigurationList({
+      a: row._serial_no
+    })
+    .then((res: any) => {
+      if (res.code === 200) {
+        tableData.value.data = tableData.value.data?.filter(
+          (item) => item._serial_no !== row._serial_no
+        )
+        emits('gettabledatalength', tableData.value.data?.length)
+      }
+    })
 }
 
 // 编辑表格数据
 const handleEdit = (row: any) => {
   router.push({
-    path: '/destination-delivery/ConfiguRations/CreateNewRule',
-    query: {a: row._serial_no}
+    path: '/destination-delivery/configurations/create-new-rule',
+    query: { a: row._serial_no }
   })
 }
 // 添加新规则
 const clickAddNewRule = () => {
-  router.push('/destination-delivery/ConfiguRations/CreateNewRule')
+  router.push('/destination-delivery/configurations/create-new-rule')
 }
 
 onMounted(() => {
@@ -134,7 +136,7 @@ onMounted(() => {
   <div class="SettingTable">
     <vxe-grid
       ref="tableRef"
-      :style="{ border: 'none'}"
+      :style="{ border: 'none' }"
       v-bind="tableData"
       :height="props.height"
       @cell-dblclick="({ row }) => handleEdit(row)"
@@ -146,14 +148,17 @@ onMounted(() => {
           <div class="empty-text">
             Configure available destination delivery regions and time slots.
           </div>
-          <el-button class="el-button--main" style="width: 117px; height: 40px;" @click="clickAddNewRule">+ Add Rule</el-button>
+          <el-button
+            class="el-button--main"
+            style="width: 117px; height: 40px"
+            @click="clickAddNewRule"
+            >+ Add Rule</el-button
+          >
         </div>
       </template>
       <!-- Tracking No字段的插槽 -->
       <template #countryNo="{ row, column }">
-        <span
-          style="color: var(--color-theme)"
-        >
+        <span style="color: var(--color-theme)">
           {{ row[column.field] }}
         </span>
       </template>

+ 275 - 227
src/views/DestinationDelivery/src/components/ConfiguRations/src/components/CreateNewRule.vue

@@ -42,104 +42,107 @@ const CountryCheckboxList = ref([])
 
 // 页面初始化
 const InitRuleData = () => {
-  if ( a!= undefined ) {
+  if (a != undefined) {
     $api
-    .InitCreateRule({
-      a: a
-    })
-    .then((res: any) => {
-      if (res.code === 200) {
-        const { returnData } = res.data
-        KLNPLCvalue.value = returnData.KLNPLCvalue
-        selectedCountry.value = returnData.country
-        setbookingdata.value = returnData.SetBookingWindow
-        windowRadio.value = returnData.SetBookingWindow.windowradio
-        if(windowRadio.value != 1) {
-          windowBeforeDays.value = returnData.booking_window_date_start
-          windowAfterDays.value = returnData.booking_window_date_end
-        }
-        recommendRadio.value = returnData.RcommendDeliveryDate.Recommendradio
-        if(recommendRadio.value != 1) {
-          recommendCheckedList.value = returnData.RcommendDeliveryDate.RecommendCheckedList
-          recommendCheckedAirList.value = returnData.RcommendDeliveryDate.RecommendCheckedAirList
-          recommendCheckedSeaList.value = returnData.RcommendDeliveryDate.RecommendCheckedSeaList
+      .InitCreateRule({
+        a: a
+      })
+      .then((res: any) => {
+        if (res.code === 200) {
+          const { returnData } = res.data
+          KLNPLCvalue.value = returnData.KLNPLCvalue
+          selectedCountry.value = returnData.country
+          setbookingdata.value = returnData.SetBookingWindow
+          windowRadio.value = returnData.SetBookingWindow.windowradio
+          if (windowRadio.value != 1) {
+            windowBeforeDays.value = returnData.booking_window_date_start
+            windowAfterDays.value = returnData.booking_window_date_end
+          }
+          recommendRadio.value = returnData.RcommendDeliveryDate.Recommendradio
+          if (recommendRadio.value != 1) {
+            recommendCheckedList.value = returnData.RcommendDeliveryDate.RecommendCheckedList
+            recommendCheckedAirList.value = returnData.RcommendDeliveryDate.RecommendCheckedAirList
+            recommendCheckedSeaList.value = returnData.RcommendDeliveryDate.RecommendCheckedSeaList
+          }
+          countryCheckedList.value = returnData.station
+          CountryCheckboxList.value = returnData.CountryCheckedList
+          // recommendata.value = returnData.RcommendDeliveryDate
         }
-        countryCheckedList.value = returnData.station
-        CountryCheckboxList.value = returnData.CountryCheckedList
-        recommendata.value = returnData.RcommendDeliveryDate
-      }
-    })
+      })
   }
 }
 
-
 const CreateRuleDisabled = computed(() => {
-  // 1. 检查基本条件是否满足
+  // 1. 检查基本条件是否满足 || recommendRadio.value === undefined
   if (
     countryCheckedList.value.length === 0 ||
     selectedCountry.value === '' ||
     windowRadio.value === undefined ||
-    recommendRadio.value === undefined ||
     KLNPLCvalue.value.length === 0
   ) {
-    return true;
+    return true
   }
 
   // 2. 处理时间窗口条件
   if (windowRadio.value !== 1) {
     // 当 windowRadio 不为 1 时,需要验证时间窗口字段
     if (windowBeforeDays.value === '' || windowAfterDays.value === '') {
-      return true;
+      return true
     }
   }
 
   // 3. 处理推荐日期条件
-  if (recommendRadio.value !== 1) {
-    // 3.1 确保至少选择了一个运输方式
-    if (recommendCheckedList.value.length === 0) {
-      return true;
-    }
+  // if (recommendRadio.value !== 1) {
+  //   // 3.1 确保至少选择了一个运输方式
+  //   if (recommendCheckedList.value.length === 0) {
+  //     return true
+  //   }
 
-    // 3.2 验证航空规则(如果选择了 Air)
-    if (recommendCheckedList.value.includes('Air')) {
-      const isAirValid = recommendCheckedAirList.value.every(item => 
-        item.ports.length > 0 && 
-        item.recommended_delivery_from !== '' && 
-        item.recommended_delivery_to !== ''
-      );
-      
-      if (!isAirValid) return true;
-    }
+  //   // 3.2 验证航空规则(如果选择了 Air)
+  //   if (recommendCheckedList.value.includes('Air')) {
+  //     const isAirValid = recommendCheckedAirList.value.every(
+  //       (item) =>
+  //         item.ports.length > 0 &&
+  //         item.recommended_delivery_from !== '' &&
+  //         item.recommended_delivery_to !== ''
+  //     )
 
-    // 3.3 验证海运规则(如果选择了 Sea)
-    if (recommendCheckedList.value.includes('Sea')) {
-      const isSeaValid = recommendCheckedSeaList.value.every(item => 
-        item.ports.length > 0 && 
-        item.carrier.length > 0 && 
-        item.recommended_delivery_from != '' && 
-        item.recommended_delivery_to != ''
-      );
-      
-      if (!isSeaValid) return true;
-    }
-  }
+  //     if (!isAirValid) return true
+  //   }
+
+  //   // 3.3 验证海运规则(如果选择了 Sea)
+  //   if (recommendCheckedList.value.includes('Sea')) {
+  //     const hasInvalidSeaItem = recommendCheckedSeaList.value.some((item) => {
+  //       const hasValidDeliveryTime = item.recommended_delivery_from && item.recommended_delivery_to
+
+  //       const hasRequiredFields =
+  //         item.rule_type !== 'Single Dimension'
+  //           ? item.ports.length > 0 && item.carrier.length > 0
+  //           : item.ports.length > 0 || item.carrier.length > 0
+
+  //       return !hasValidDeliveryTime || !hasRequiredFields
+  //     })
+
+  //     if (hasInvalidSeaItem) return true
+  //   }
+  // }
   // 4. 所有条件都满足,返回 false(不禁用)
-  return false;
-});
-// select country 
-const handleClickSelectCountry = (val:any) =>{
+  return false
+})
+// select country
+const handleClickSelectCountry = (val: any) => {
   selectedCountry.value = val
   countryCheckedList.value = []
 }
 // select station list
-const handleChangeStation = (val:any) =>{
+const handleChangeStation = (val: any) => {
   countryCheckedList.value = val
 }
 
 // select booking window
 const bookingWindow = ref('')
 const bookingdetail = ref('')
-const changeBookingWindow = (radio: number, beforedays:any, afterdays:any) => {
+const changeBookingWindow = (radio: number, beforedays: any, afterdays: any) => {
   windowRadio.value = radio
   windowBeforeDays.value = beforedays
   windowAfterDays.value = afterdays
@@ -163,10 +166,11 @@ const querySearchAsync = (query: string) => {
   if (query) {
     loading.value = true
     setTimeout(() => {
-      $api.getKLNEmployeeList({ 
-        term: query,
-        station: countryCheckedList.value
-      })
+      $api
+        .getKLNEmployeeList({
+          term: query,
+          station: countryCheckedList.value
+        })
         .then((res: any) => {
           if (res.code === 200) {
             loading.value = false
@@ -183,17 +187,17 @@ const querySearchAsync = (query: string) => {
 }
 // 保存
 const handleSubmitRule = () => {
-  const airlist = recommendCheckedAirList.value.map(item => {
-    const {PortList, ...rest} = item
-    if(recommendRadio.value == 2) {
+  const airlist = recommendCheckedAirList.value.map((item) => {
+    const { PortList, ...rest } = item
+    if (recommendRadio.value == 2) {
       return rest
     } else {
       return []
     }
   })
-  const seaList = recommendCheckedSeaList.value.map(item => {
-    const {PortList,CarrierList, ...rest} = item
-    if(recommendRadio.value == 2) {
+  const seaList = recommendCheckedSeaList.value.map((item) => {
+    const { PortList, CarrierList, ...rest } = item
+    if (recommendRadio.value == 2) {
       return rest
     } else {
       return []
@@ -202,39 +206,59 @@ const handleSubmitRule = () => {
   let airData = []
   let airlistInfo = {}
   let mergeData = []
-  if(windowRadio.value == 1) {
+  if (windowRadio.value == 1) {
     bookingWindow.value = 'No_Restrictions'
     bookingdetail.value = 'No Specific time restrictions for creating booking'
-  } else if(windowRadio.value == 2) {
+  } else if (windowRadio.value == 2) {
     bookingWindow.value = 'Restrictions_ETD_ATD'
-    bookingdetail.value = 'ETD/ATD: ' + windowBeforeDays.value + ' days before to ' + windowAfterDays.value + ' days after'
-  } else if(windowRadio.value == 3) {
+    bookingdetail.value =
+      'ETD/ATD: ' +
+      windowBeforeDays.value +
+      ' days before to ' +
+      windowAfterDays.value +
+      ' days after'
+  } else if (windowRadio.value == 3) {
     bookingWindow.value = 'Restrictions_ETA_ATA'
-    bookingdetail.value = 'ETA/ATA: ' + windowBeforeDays.value + ' days before to ' + windowAfterDays.value + ' days after'
+    bookingdetail.value =
+      'ETA/ATA: ' +
+      windowBeforeDays.value +
+      ' days before to ' +
+      windowAfterDays.value +
+      ' days after'
   }
-  if(recommendRadio.value == 1) {
-    recommendDelivery.value = 'No_Recommended'
-    recommenddetail.value = 'No Specific recommended time for choosing delivery date'
-  } else {
-    recommendDelivery.value = 'Delivery_ETA_ATA'
-    if(recommendCheckedList.value.includes('Air')) {
-      recommenddetail.value += 'Air:\nDefault Rule- Air Port: ALL,\nRecommend Delivery Date: ETA/ATA+' + airlist[0].recommended_delivery_from + ' Days to ETA/ATA+'+ airlist[0].recommended_delivery_to + ' Days;\n'
-      airlist.forEach((item) => {
-        item.ports = item.ports.join(',')
-        item.carrier = ''
-      })
-      mergeData = [...airlist]
-    }
-    if(recommendCheckedList.value.includes('Sea')) {
-      recommenddetail.value += 'Sea:\nDefault Rule- ort: ALL, Carrier: ALL,\nRecommend Delivery Date: ETA/ATA+' + seaList[0].recommended_delivery_from + ' Days to ETA/ATA+'+ seaList[0].recommended_delivery_to + ' Days;' 
-      seaList.forEach((item) => {
-        item.ports = item.ports.join(',')
-        item.carrier = item.carrier.join(',')
-      })
-      mergeData = [...mergeData , ...seaList]
-    }
-  }
-  if(seaList.length != 0) {
+  // if (recommendRadio.value == 1) {
+  //   recommendDelivery.value = 'No_Recommended'
+  //   recommenddetail.value = 'No Specific recommended time for choosing delivery date'
+  // } else {
+  //   recommendDelivery.value = 'Delivery_ETA_ATA'
+  //   if (recommendCheckedList.value.includes('Air')) {
+  //     recommenddetail.value +=
+  //       'Air:\nDefault Rule- Air Port: ALL,\nRecommend Delivery Date: ETA/ATA+' +
+  //       airlist[0].recommended_delivery_from +
+  //       ' Days to ETA/ATA+' +
+  //       airlist[0].recommended_delivery_to +
+  //       ' Days;\n'
+  //     airlist.forEach((item) => {
+  //       item.ports = item.ports.join(',')
+  //       item.carrier = ''
+  //     })
+  //     mergeData = [...airlist]
+  //   }
+  //   if (recommendCheckedList.value.includes('Sea')) {
+  //     recommenddetail.value +=
+  //       'Sea:\nDefault Rule- ort: ALL, Carrier: ALL,\nRecommend Delivery Date: ETA/ATA+' +
+  //       seaList[0].recommended_delivery_from +
+  //       ' Days to ETA/ATA+' +
+  //       seaList[0].recommended_delivery_to +
+  //       ' Days;'
+  //     seaList.forEach((item) => {
+  //       item.ports = item.ports.join(',')
+  //       item.carrier = item.carrier.join(',')
+  //     })
+  //     mergeData = [...mergeData, ...seaList]
+  //   }
+  // }
+  if (seaList.length != 0) {
     airData = Object.keys(seaList?.[0])
     airData.forEach((item) => {
       Object.assign(airlistInfo, {
@@ -242,36 +266,37 @@ const handleSubmitRule = () => {
       })
     })
   }
-  $api.handelSaveRule({
-    serial_no: a != undefined ? a: '',
-    country: selectedCountry.value,
-    station: countryCheckedList.value,
-    booking_window: bookingWindow.value,
-    booking_window_date_start: windowBeforeDays.value,
-    booking_window_date_end: windowAfterDays.value,
-    recommended_delivery: recommendDelivery.value,
-    booking_window_desc: bookingdetail.value,
-    kln_pic: KLNPLCvalue.value,
-    recommended_delivery_date_desc: recommenddetail.value,
-    ...airlistInfo
-  }).then((res: any) => {
-    if (res.code === 200 && res.data.msg == 'success') {
-      SaveedVisible.value = true
-      setTimeout(() => {
-        SaveedVisible.value = false
-        router.push({ name: 'Configurations'})
-      }, 3000)
-    } else {
+  $api
+    .handelSaveRule({
+      serial_no: a != undefined ? a : '',
+      country: selectedCountry.value,
+      station: countryCheckedList.value,
+      booking_window: bookingWindow.value,
+      booking_window_date_start: windowBeforeDays.value,
+      booking_window_date_end: windowAfterDays.value,
+      // recommended_delivery: recommendDelivery.value,
+      booking_window_desc: bookingdetail.value,
+      kln_pic: KLNPLCvalue.value,
+      // recommended_delivery_date_desc: recommenddetail.value,
+      ...airlistInfo
+    })
+    .then((res: any) => {
+      if (res.code === 200 && res.data.msg == 'success') {
+        SaveedVisible.value = true
+        setTimeout(() => {
+          SaveedVisible.value = false
+          router.push({ name: 'Configurations' })
+        }, 3000)
+      } else {
         UnableSaveVisible.value = true
         missingmessage.value = res.data.msg
-    }
-  })
+      }
+    })
 }
 
 onMounted(() => {
   InitRuleData()
 })
-
 </script>
 
 <template>
@@ -280,11 +305,20 @@ onMounted(() => {
       <div v-if="a != undefined">Modify Rule</div>
       <div v-else>Create New Rule</div>
       <div class="operator">
-        <el-button @click="CancelRulesVisible = true" style="height: 40px; width: 115px" type="default">
+        <el-button
+          @click="CancelRulesVisible = true"
+          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" :disabled="CreateRuleDisabled" @click="handleSubmitRule">
+        <el-button
+          style="height: 40px; width: 120px"
+          class="el-button--main el-button--pain-theme"
+          :disabled="CreateRuleDisabled"
+          @click="handleSubmitRule"
+        >
           <span
             style="
               display: inline-block;
@@ -311,12 +345,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>
@@ -337,19 +367,17 @@ 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>
         </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>
@@ -358,107 +386,107 @@ onMounted(() => {
       <div class="setting-top-title">Setting</div>
       <el-collapse v-model="activeRules" @change="IsFirstActive = !IsFirstActive">
         <el-collapse-item name="SelectStation">
-            <template #title>
-              <div class="Rules_Title">
-                <span class="iconfont_icon icon_dark">
-                  <svg class="iconfont" aria-hidden="true">
-                    <use
-                      :xlink:href="IsFirstActive ? '#icon-icon_dropdown_b' : '#icon-icon_up_b'"
-                    ></use>
-                  </svg>
-                </span>
-                <span class="stars_red">*</span>Select Station (Enable Booking)
-              </div>
-            </template>
-            <div>
-              <SelectStation
+          <template #title>
+            <div class="Rules_Title">
+              <span class="iconfont_icon icon_dark">
+                <svg class="iconfont" aria-hidden="true">
+                  <use
+                    :xlink:href="IsFirstActive ? '#icon-icon_dropdown_b' : '#icon-icon_up_b'"
+                  ></use>
+                </svg>
+              </span>
+              <span class="stars_red">*</span>Select Station (Enable Booking)
+            </div>
+          </template>
+          <div>
+            <SelectStation
               @handleClickSelectCountry="handleClickSelectCountry"
               @handleChangeStation="handleChangeStation"
               :CheckboxList="CountryCheckboxList"
               :CheckedList="countryCheckedList"
               :SelectCountry="selectedCountry"
-              ></SelectStation>
-            </div>
-          </el-collapse-item>
+            ></SelectStation>
+          </div>
+        </el-collapse-item>
       </el-collapse>
       <el-collapse v-model="activeRules" @change="IsTwoActive = !IsTwoActive">
         <el-collapse-item name="SelectBooking">
-            <template #title>
-              <div class="Rules_Title">
-                <span class="iconfont_icon icon_dark">
-                  <svg class="iconfont" aria-hidden="true">
-                    <use
-                      :xlink:href="IsTwoActive ? '#icon-icon_dropdown_b' : '#icon-icon_up_b'"
-                    ></use>
-                  </svg>
-                </span>
-                <span class="stars_red">*</span>Set Booking Window
-              </div>
-            </template>
-            <div>
-              <SetBookingWindow
-                :setbookingdata="setbookingdata"
-                @changeBookingWindow="changeBookingWindow"
-              ></SetBookingWindow>
+          <template #title>
+            <div class="Rules_Title">
+              <span class="iconfont_icon icon_dark">
+                <svg class="iconfont" aria-hidden="true">
+                  <use
+                    :xlink:href="IsTwoActive ? '#icon-icon_dropdown_b' : '#icon-icon_up_b'"
+                  ></use>
+                </svg>
+              </span>
+              <span class="stars_red">*</span>Set Booking Window
             </div>
-          </el-collapse-item>
+          </template>
+          <div>
+            <SetBookingWindow
+              :setbookingdata="setbookingdata"
+              @changeBookingWindow="changeBookingWindow"
+            ></SetBookingWindow>
+          </div>
+        </el-collapse-item>
       </el-collapse>
-      <el-collapse v-model="activeRules" @change="IsThreeActive = !IsThreeActive">
+      <!-- <el-collapse v-model="activeRules" @change="IsThreeActive = !IsThreeActive">
         <el-collapse-item name="RecommendDeliveryDate">
-            <template #title>
-              <div class="Rules_Title">
-                <span class="iconfont_icon icon_dark">
-                  <svg class="iconfont" aria-hidden="true">
-                    <use
-                      :xlink:href="IsThreeActive ? '#icon-icon_dropdown_b' : '#icon-icon_up_b'"
-                    ></use>
-                  </svg>
-                </span>
-                <span class="stars_red">*</span>Recommend Delivery Date 
-              </div>
-            </template>
-            <div>
-              <RecommendDate
-                :recommendata="recommendata"
-                @chackchangerecommend="checkRecommend"
-              ></RecommendDate>
+          <template #title>
+            <div class="Rules_Title">
+              <span class="iconfont_icon icon_dark">
+                <svg class="iconfont" aria-hidden="true">
+                  <use
+                    :xlink:href="IsThreeActive ? '#icon-icon_dropdown_b' : '#icon-icon_up_b'"
+                  ></use>
+                </svg>
+              </span>
+              <span class="stars_red">*</span>Recommend Delivery Date
             </div>
-          </el-collapse-item>
-      </el-collapse>
+          </template>
+          <div>
+            <RecommendDate
+              :recommendata="recommendata"
+              @chackchangerecommend="checkRecommend"
+            ></RecommendDate>
+          </div>
+        </el-collapse-item>
+      </el-collapse> -->
       <el-collapse v-model="activeRules" @change="IsFourActive = !IsFourActive">
         <el-collapse-item name="KLNPLC">
-            <template #title>
-              <div class="Rules_Title">
-                <span class="iconfont_icon icon_dark">
-                  <svg class="iconfont" aria-hidden="true">
-                    <use
-                      :xlink:href="IsFourActive ? '#icon-icon_dropdown_b' : '#icon-icon_up_b'"
-                    ></use>
-                  </svg>
-                </span>
-                <span class="stars_red">*</span>KLN PIC
-              </div>
-            </template>
-            <div>
-              <el-select
-                v-model="KLNPLCvalue"
-                filterable
-                remote
-                multiple
-                placeholder="Select Employee Account"
-                :remote-method="querySearchAsync"
-                :loading="loading"
-                style="width: 400px;margin-bottom: 5px;"
-              >
-                <el-option
-                  v-for="item in options"
-                  :key="item.value"
-                  :label="item.label"
-                  :value="item.value"
-                />
-              </el-select>
+          <template #title>
+            <div class="Rules_Title">
+              <span class="iconfont_icon icon_dark">
+                <svg class="iconfont" aria-hidden="true">
+                  <use
+                    :xlink:href="IsFourActive ? '#icon-icon_dropdown_b' : '#icon-icon_up_b'"
+                  ></use>
+                </svg>
+              </span>
+              <span class="stars_red">*</span>KLN PIC
             </div>
-          </el-collapse-item>
+          </template>
+          <div>
+            <el-select
+              v-model="KLNPLCvalue"
+              filterable
+              remote
+              multiple
+              placeholder="Select Employee Account"
+              :remote-method="querySearchAsync"
+              :loading="loading"
+              style="width: 400px; margin-bottom: 5px"
+            >
+              <el-option
+                v-for="item in options"
+                :key="item.value"
+                :label="item.label"
+                :value="item.value"
+              />
+            </el-select>
+          </div>
+        </el-collapse-item>
       </el-collapse>
     </div>
   </div>
@@ -544,7 +572,7 @@ onMounted(() => {
   box-shadow: none;
   border: 1px solid var(--color-theme);
 }
-:deep(.el-select__wrapper.is-focused ){
+:deep(.el-select__wrapper.is-focused) {
   box-shadow: none;
   border: 1px solid var(--color-theme);
 }
@@ -574,4 +602,24 @@ onMounted(() => {
 :deep(.el-dialog__body) {
   font-weight: 400;
 }
-</style>
+
+.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>

+ 245 - 150
src/views/DestinationDelivery/src/components/ConfiguRations/src/components/RecommendDate.vue

@@ -1,11 +1,12 @@
 <script setup lang="ts">
 import SelectValue from './SelectValue.vue'
 import { ref } from 'vue'
+import { cloneDeep } from 'lodash'
 
 // 定义类型接口
 interface RuleOption {
-  label: string;
-  value: string;
+  label: string
+  value: string
 }
 interface PortOption {
   value: string
@@ -13,21 +14,20 @@ interface PortOption {
   checked: boolean
 }
 
-
 interface RuleItem {
-  priority: string;
-  rule_type: string;
-  mode_type: string;
-  ports?: string[];
-  carrier?: string[];
-  PortList?:PortOption[];
-  CarrierList?:PortOption[];
-  recommended_delivery_from: string;
-  recommended_delivery_to: string;
+  priority: string
+  rule_type: string
+  mode_type: string
+  ports?: string[]
+  carrier?: string[]
+  PortList?: PortOption[]
+  CarrierList?: PortOption[]
+  recommended_delivery_from: string
+  recommended_delivery_to: string
 }
 
 // 定义 RuleItem 中数组字段的类型
-type ArrayFields = 'ports' | 'carrier';
+type ArrayFields = 'ports' | 'carrier'
 
 const props = defineProps({
   recommendata: {
@@ -45,9 +45,7 @@ const isAir = ref(false)
 const isSea = ref(false)
 const RecommendCheckedList = ref<string[]>([])
 // 选项配置
-const AirRuleTypeoptions = ref<RuleOption[]>([
-  { label: 'Specific Rule', value: 'Specific Rule' }
-])
+const AirRuleTypeoptions = ref<RuleOption[]>([{ label: 'Specific Rule', value: 'Specific Rule' }])
 
 const RuleTypeoptions = ref<RuleOption[]>([
   { label: 'Specific Rule', value: 'Specific Rule' },
@@ -63,7 +61,7 @@ const AirContentList = ref<RuleItem[]>([
     recommended_delivery_from: '',
     recommended_delivery_to: '',
     mode_type: 'air',
-    PortList:[]
+    PortList: []
   }
 ])
 const SeaContentList = ref<RuleItem[]>([
@@ -75,36 +73,39 @@ const SeaContentList = ref<RuleItem[]>([
     recommended_delivery_from: '',
     recommended_delivery_to: '',
     mode_type: 'sea',
-    PortList:[],
+    PortList: [],
     CarrierList: []
   }
 ])
 
-const recommendata = ref(props.recommendata)
+const recommendata = ref()
 
 const initRecommendData = () => {
-  if(recommendata.value) {
+  if (recommendata.value) {
     Recommendradio.value = recommendata.value.Recommendradio
-    if(Recommendradio.value == 2) {
+    if (Recommendradio.value == 2) {
       isRecommendETA.value = true
       RecommendCheckedList.value = recommendata.value.RecommendCheckedList
-      if(RecommendCheckedList.value.includes('Air')) {
+      if (RecommendCheckedList.value.includes('Air')) {
         isAir.value = true
       }
-      if(RecommendCheckedList.value.includes('Sea')) {
+      if (RecommendCheckedList.value.includes('Sea')) {
         isSea.value = true
       }
       AirContentList.value = recommendata.value.RecommendCheckedAirList
       SeaContentList.value = recommendata.value.RecommendCheckedSeaList
     }
-    }
+  }
 }
 
-watch(() => props.recommendata, (val) => { 
-  recommendata.value = val
-  initRecommendData()
-}, { immediate: true, deep: true })
-
+watch(
+  () => props.recommendata,
+  (val) => {
+    recommendata.value = cloneDeep(val)
+    initRecommendData()
+  },
+  { immediate: true, deep: true }
+)
 
 // 创建规则项的工厂函数
 function createRuleItem(type: 'Air' | 'Sea', ruleType: string): RuleItem {
@@ -112,13 +113,13 @@ function createRuleItem(type: 'Air' | 'Sea', ruleType: string): RuleItem {
     priority: 'P1',
     rule_type: ruleType,
     recommended_delivery_from: '',
-    recommended_delivery_to: '',
+    recommended_delivery_to: ''
   }
   if (type === 'Air') {
     return {
       ...baseItem,
       ports: ruleType === '*Default Rule' ? ['ALL'] : [],
-      mode_type: 'air',
+      mode_type: 'air'
       // PortList: JSON.parse(JSON.stringify(AirPorList.value))
     }
   }
@@ -126,7 +127,7 @@ function createRuleItem(type: 'Air' | 'Sea', ruleType: string): RuleItem {
     ...baseItem,
     ports: ruleType === '*Default Rule' ? ['ALL'] : [],
     carrier: ruleType === '*Default Rule' ? ['ALL'] : [],
-    mode_type: 'sea',
+    mode_type: 'sea'
     // PortList: JSON.parse(JSON.stringify(SeaPortList.value)),
     // CarrierList: JSON.parse(JSON.stringify(SeaCarrierList.value))
   }
@@ -149,56 +150,68 @@ const CheckChange = (val: string[]) => {
 
 const handleCheckboxClick = (event: Event) => {
   const target = event.target as HTMLElement
-  const isCheckboxInput = target.closest('.el-checkbox__inner')
+  const isCheckboxInput = target.closest('.el-checkbox__input')
   const isCheckboxTitle = target.closest('.titlecheckbox')
   if (!isCheckboxInput && !isCheckboxTitle) {
     event.preventDefault()
   }
 }
+
 // 选择booking window
 const ChangeFrequency = (val: number) => {
   isRecommendETA.value = val === 2
-  emits('chackchangerecommend', RecommendCheckedList.value, AirContentList.value, SeaContentList.value, Recommendradio.value)
+  emits(
+    'chackchangerecommend',
+    RecommendCheckedList.value,
+    AirContentList.value,
+    SeaContentList.value,
+    Recommendradio.value
+  )
 }
 // 修复后的 handleInput 函数
-const handleInput = (val: string, index: number, type: 'recommended_delivery_from' | 'recommended_delivery_to', list: RuleItem[]) => {
+const handleInput = (
+  val: string,
+  index: number,
+  type: 'recommended_delivery_from' | 'recommended_delivery_to',
+  list: RuleItem[]
+) => {
   // 移除非数字字符
-  const numStr = val.replace(/[^\d]/g, '');
+  const numStr = val.replace(/[^\d]/g, '')
   // 处理空值情况
   if (numStr === '') {
-    list[index][type] = '';
-    return;
+    list[index][type] = ''
+    return
   }
-  
+
   // 转换为数字以进行范围检查
-  const num = parseInt(numStr, 10);
-  
+  const num = parseInt(numStr, 10)
+
   // 确保最小值为1(但保持为字符串形式)
   if (num < 1) {
-    list[index][type] = '1';
+    list[index][type] = '1'
   } else {
     // 保持为字符串形式
-    list[index][type] = numStr;
+    list[index][type] = numStr
   }
-};
+}
 // 删除数据
 const handleDelete = (index: number, list: RuleItem[], type: 'Air' | 'Sea') => {
-  list.splice(index, 1);
+  list.splice(index, 1)
   if (list.length === 0) {
     if (type === 'Air') {
       isAir.value = false
-      RecommendCheckedList.value = RecommendCheckedList.value.filter(item => item !== 'Air')
+      RecommendCheckedList.value = RecommendCheckedList.value.filter((item) => item !== 'Air')
     } else {
       isSea.value = false
-      RecommendCheckedList.value = RecommendCheckedList.value.filter(item => item !== 'Sea')
+      RecommendCheckedList.value = RecommendCheckedList.value.filter((item) => item !== 'Sea')
     }
   }
   updatePriorities()
-};
+}
 // 添加数据
 const AddRuleItem = (list: RuleItem[], type: 'Air' | 'Sea') => {
   // 检查是否已存在默认规则
-  const hasDefaultRule = list.some(item => item.rule_type === '*Default Rule')
+  const hasDefaultRule = list.some((item) => item.rule_type === '*Default Rule')
   // 如果已经有默认规则,则创建特定规则
   const ruleType = hasDefaultRule ? 'Specific Rule' : '*Default Rule'
   list.push(createRuleItem(type, ruleType))
@@ -206,15 +219,21 @@ const AddRuleItem = (list: RuleItem[], type: 'Air' | 'Sea') => {
 }
 // 根据RuleType的值来修改Priority的值
 const updatePriorities = () => {
-  emits('chackchangerecommend', RecommendCheckedList.value, AirContentList.value, SeaContentList.value,Recommendradio.value)
+  emits(
+    'chackchangerecommend',
+    RecommendCheckedList.value,
+    AirContentList.value,
+    SeaContentList.value,
+    Recommendradio.value
+  )
   updateListPriorities(AirContentList.value, 'Air')
   updateListPriorities(SeaContentList.value, 'Sea')
-};
+}
 // 统一更新列表优先级
 const updateListPriorities = (list: RuleItem[], type: 'Air' | 'Sea') => {
   const length = list.length
   // 保护默认规则的数据
-  list.forEach(item => {
+  list.forEach((item) => {
     if (item.rule_type === '*Default Rule') {
       if (type === 'Air') {
         item.ports = ['ALL']
@@ -234,40 +253,43 @@ const updateListPriorities = (list: RuleItem[], type: 'Air' | 'Sea') => {
 }
 // 处理长度为1
 const handleLengthOne = (list: RuleItem[], type: string) => {
-  list.forEach(item => item.priority = 'P1')
-};
+  list.forEach((item) => (item.priority = 'P1'))
+}
 // 处理长度为2
 const handleLengthTwo = (list: RuleItem[], type: string) => {
-  const types = new Set(list.map(i => i.rule_type))
+  const types = new Set(list.map((i) => i.rule_type))
   // 两个都是 *Default Rule
   if (types.size === 1 && types.has('*Default Rule')) {
-    list.forEach(item => item.priority = 'P1')
+    list.forEach((item) => (item.priority = 'P1'))
     return
   }
   // 包含 *Default Rule 和其他类型
   if (types.has('*Default Rule')) {
-    list.forEach(item => {
+    list.forEach((item) => {
       item.priority = item.rule_type === '*Default Rule' ? 'P2' : 'P1'
     })
     return
   }
   // 同时存在 Specific Rule 和 Single Dimension
   if (types.has('Specific Rule') && types.has('Single Dimension')) {
-    list.forEach(item => {
+    list.forEach((item) => {
       item.priority = item.rule_type === 'Specific Rule' ? 'P1' : 'P2'
     })
     return
   }
   // 其他情况
-  list.forEach(item => item.priority = 'P1')
-};
+  list.forEach((item) => (item.priority = 'P1'))
+}
 // 处理长度≥3
 const handleLengthThreePlus = (list: RuleItem[], type: string) => {
   // 统计各类型数量
-  const counts = list.reduce((acc, cur) => {
-    acc[cur.rule_type] = (acc[cur.rule_type] || 0) + 1
-    return acc
-  }, {} as Record<string, number>)
+  const counts = list.reduce(
+    (acc, cur) => {
+      acc[cur.rule_type] = (acc[cur.rule_type] || 0) + 1
+      return acc
+    },
+    {} as Record<string, number>
+  )
   // 获取所有存在的类型
   const existingTypes = Object.keys(counts)
   // 三个不同类型都存在
@@ -281,28 +303,30 @@ const handleLengthThreePlus = (list: RuleItem[], type: string) => {
       'Single Dimension': 'P2',
       '*Default Rule': 'P3'
     }
-    list.forEach(item => {
+    list.forEach((item) => {
       item.priority = priorityMap[item.rule_type]
     })
     return
   }
   // 全为同一种类型的情况
   if (existingTypes.length === 1) {
-    list.forEach(item => item.priority = 'P1')
+    list.forEach((item) => (item.priority = 'P1'))
     return
   }
   // 处理 Specific + Default 组合
-  if (existingTypes.length === 2 && 
-      existingTypes.includes('Specific Rule') && 
-      existingTypes.includes('*Default Rule')) {
-    list.forEach(item => {
+  if (
+    existingTypes.length === 2 &&
+    existingTypes.includes('Specific Rule') &&
+    existingTypes.includes('*Default Rule')
+  ) {
+    list.forEach((item) => {
       item.priority = item.rule_type === 'Specific Rule' ? 'P1' : 'P2'
     })
     return
   }
   // 存在两个Default Rule
   if (counts['*Default Rule'] === 2 && existingTypes.length === 2) {
-    list.forEach(item => {
+    list.forEach((item) => {
       item.priority = item.rule_type === '*Default Rule' ? 'P2' : 'P1'
     })
     return
@@ -311,12 +335,12 @@ const handleLengthThreePlus = (list: RuleItem[], type: string) => {
   if (counts['Single Dimension'] === 2) {
     if (existingTypes.includes('*Default Rule')) {
       // 两个Single + 一个Default
-      list.forEach(item => {
+      list.forEach((item) => {
         item.priority = item.rule_type === '*Default Rule' ? 'P2' : 'P1'
       })
     } else if (existingTypes.includes('Specific Rule')) {
       // 两个Single + 一个Specific
-      list.forEach(item => {
+      list.forEach((item) => {
         item.priority = item.rule_type === 'Specific Rule' ? 'P1' : 'P2'
       })
     }
@@ -328,33 +352,39 @@ const handleLengthThreePlus = (list: RuleItem[], type: string) => {
     'Single Dimension': 'P2',
     '*Default Rule': 'P3'
   }
-  list.forEach(item => {
+  list.forEach((item) => {
     item.priority = defaultPriorityMap[item.rule_type] || 'P3'
   })
 }
 // 修复:改变选项值 - 使用类型保护
-const changeSelectedValue = (val: string[], index: number, field: ArrayFields, list: RuleItem[]) => {
-  const item = list[index] as Record<ArrayFields, string[]>;
-  item[field] = val;
+const changeSelectedValue = (
+  val: string[],
+  index: number,
+  field: ArrayFields,
+  list: RuleItem[]
+) => {
+  const item = list[index] as Record<ArrayFields, string[]>
+  item[field] = val
   // 新增逻辑:检查是否从 Single Dimension 变为 Specific Rule
   if (item['rule_type'] != '*Default Rule') {
     if (item['mode_type'] === 'air') {
       // Air 规则:只检查 ports
       if (item.ports && item.ports.length > 0 && !item.ports.includes('ALL')) {
-        item['rule_type'] = 'Specific Rule';
-        updatePriorities();
+        item['rule_type'] = 'Specific Rule'
+        updatePriorities()
       }
     } else if (item['mode_type'] === 'sea') {
       // Sea 规则:检查 ports 和 carrier
-      const portsSelected = item.ports && item.ports.length > 0 && !item.ports.includes('ALL');
-      const carrierSelected = item.carrier && item.carrier.length > 0 && !item.carrier.includes('ALL');
-      
+      const portsSelected = item.ports && item.ports.length > 0 && !item.ports.includes('ALL')
+      const carrierSelected =
+        item.carrier && item.carrier.length > 0 && !item.carrier.includes('ALL')
+
       if (portsSelected && carrierSelected) {
-        item['rule_type'] = 'Specific Rule';
-        updatePriorities();
+        item['rule_type'] = 'Specific Rule'
+        updatePriorities()
       } else {
-        item['rule_type'] = 'Single Dimension';
-        updatePriorities();
+        item['rule_type'] = 'Single Dimension'
+        updatePriorities()
       }
     }
   }
@@ -387,32 +417,53 @@ const changeRuleType = (val: string, index: number, list: RuleItem[]) => {
           <el-checkbox-group v-model="RecommendCheckedList" @change="CheckChange">
             <!-- Air 部分 -->
             <el-checkbox class="delayedType" value="Air" @click="handleCheckboxClick">
-              <div class="titlecheckbox">
+              <div class="titlecheckbox clickable-area">
                 <div>Air</div>
-                <span class="icon_grey font_family" :class="isAir ? 'icon-icon_dropdown_b' : 'icon-icon_up_b'"></span>
+                <span
+                  class="icon_grey font_family"
+                  :class="isAir ? 'icon-icon_dropdown_b' : 'icon-icon_up_b'"
+                ></span>
               </div>
-              <div v-if="isAir" class="radiocheckbox" style="margin-top: 16px">
+              <div v-if="isAir" class="radiocheckbox" style="margin-top: 16px; padding-left: 8px">
                 <div class="AirCoulumn">
-                  <div class="AicoulumnTitile" style="width: 10%;">priority</div>
-                  <div class="AicoulumnTitile" style="width: 20%;">Rule Type</div>
-                  <div class="AicoulumnTitile" style="width: 40%;">Air Port</div>
-                  <div style="display: flex;flex-direction: column;border-right: 1px solid var(--color-system-border);width: 20%;">
+                  <div class="AicoulumnTitile" style="width: 10%">priority</div>
+                  <div class="AicoulumnTitile" style="width: 20%">Rule Type</div>
+                  <div class="AicoulumnTitile" style="width: 40%">Air Port</div>
+                  <div
+                    style="
+                      display: flex;
+                      flex-direction: column;
+                      border-right: 1px solid var(--color-system-border);
+                      width: 20%;
+                    "
+                  >
                     <div class="AicoulumnTitile2">Recommended Delivery Date</div>
-                    <div style="display: flex;height: 24px;align-items: center;">
-                      <div class="datetitle" style="border-right: 1px solid var(--color-system-border);">From (ETA/ATA + Days)</div>
+                    <div style="display: flex; height: 24px; align-items: center">
+                      <div
+                        class="datetitle"
+                        style="border-right: 1px solid var(--color-system-border)"
+                      >
+                        From (ETA/ATA + Days)
+                      </div>
                       <div class="datetitle">To (ETA/ATA + Days)</div>
                     </div>
                   </div>
-                  <div class="AirCoumlulnAdd" style="width: 10%;" @click="AddRuleItem(AirContentList, 'Air')">+ Add</div>
+                  <div
+                    class="AirCoumlulnAdd"
+                    style="width: 10%"
+                    @click="AddRuleItem(AirContentList, 'Air')"
+                  >
+                    + Add
+                  </div>
                 </div>
                 <div class="AirContent" v-for="(item, index) in AirContentList" :key="index">
-                  <div class="AirCoumlumn" style="width: 10%;">{{ item.priority }}</div>
-                  <div class="AirCoumlumn" style="width: 20%;">
+                  <div class="AirCoumlumn" style="width: 10%">{{ item.priority }}</div>
+                  <div class="AirCoumlumn" style="width: 20%">
                     <el-select
                       v-model="item.rule_type"
                       disabled
-                      style="width: 100%;"
-                      @change="val => changeRuleType(val, index, AirContentList)"
+                      style="width: 100%"
+                      @change="(val) => changeRuleType(val, index, AirContentList)"
                     >
                       <el-option
                         v-for="opt in AirRuleTypeoptions"
@@ -422,35 +473,42 @@ const changeRuleType = (val: string, index: number, list: RuleItem[]) => {
                       />
                     </el-select>
                   </div>
-                  <div class="AirCoumlumn" style="width: 40%;">
+                  <div class="AirCoumlumn" style="width: 40%">
                     <SelectValue
                       ref="AirPortRef"
                       :SelectIndex="index"
                       :SelectedValue="item.ports"
                       :typeisDisabled="item.rule_type"
                       SelectType="air"
-                      @changeSelectedValue="val => changeSelectedValue(val, index, 'ports', AirContentList)"
+                      @changeSelectedValue="
+                        (val) => changeSelectedValue(val, index, 'ports', AirContentList)
+                      "
                     />
                   </div>
-                  <div class="AirCoumlumn" style="width: 10%;">
-                    <el-input 
-                      @input="val => handleInput(val, index, 'recommended_delivery_from', AirContentList)" 
-                      placeholder="Input" 
+                  <div class="AirCoumlumn" style="width: 10%">
+                    <el-input
+                      @input="
+                        (val) =>
+                          handleInput(val, index, 'recommended_delivery_from', AirContentList)
+                      "
+                      placeholder="Input"
                       v-model="item.recommended_delivery_from"
                     />
                   </div>
-                  <div class="AirCoumlumn" style="width: 10%;">
-                    <el-input 
-                      @input="val => handleInput(val, index, 'recommended_delivery_to', AirContentList)"  
-                      placeholder="Input" 
+                  <div class="AirCoumlumn" style="width: 10%">
+                    <el-input
+                      @input="
+                        (val) => handleInput(val, index, 'recommended_delivery_to', AirContentList)
+                      "
+                      placeholder="Input"
                       v-model="item.recommended_delivery_to"
                     />
                   </div>
-                  <div class="AirDelete" style="width: 10%;">
-                    <el-button 
-                      v-if="item.rule_type !== '*Default Rule'" 
-                      @click="handleDelete(index, AirContentList, 'Air')" 
-                      class="el-button--blue" 
+                  <div class="AirDelete" style="width: 10%">
+                    <el-button
+                      v-if="item.rule_type !== '*Default Rule'"
+                      @click="handleDelete(index, AirContentList, 'Air')"
+                      class="el-button--blue"
                       style="height: 24px"
                     >
                       <span class="font_family icon-icon_delete_b"></span>
@@ -461,33 +519,54 @@ const changeRuleType = (val: string, index: number, list: RuleItem[]) => {
             </el-checkbox>
             <!-- Sea 部分 -->
             <el-checkbox class="delayedType" value="Sea" @click="handleCheckboxClick">
-              <div class="titlecheckbox">
+              <div class="titlecheckbox clickable-area">
                 <div>Sea</div>
-                <span class="icon_grey font_family" :class="isSea ? 'icon-icon_dropdown_b' : 'icon-icon_up_b'"></span>
+                <span
+                  class="icon_grey font_family"
+                  :class="isSea ? 'icon-icon_dropdown_b' : 'icon-icon_up_b'"
+                ></span>
               </div>
-              <div v-if="isSea" style="margin-top: 16px">
+              <div v-if="isSea" style="margin-top: 16px; padding-left: 8px">
                 <div class="AirCoulumn">
-                  <div class="AicoulumnTitile" style="width: 10%;">priority</div>
-                  <div class="AicoulumnTitile" style="width: 14%;">Rule Type</div>
-                  <div class="AicoulumnTitile" style="width: 23%;">Port</div>
-                  <div class="AicoulumnTitile" style="width: 23%;">Carrier</div>
-                  <div style="display: flex;flex-direction: column;border-right: 1px solid var(--color-system-border);width: 20%;">
+                  <div class="AicoulumnTitile" style="width: 10%">priority</div>
+                  <div class="AicoulumnTitile" style="width: 14%">Rule Type</div>
+                  <div class="AicoulumnTitile" style="width: 23%">Port</div>
+                  <div class="AicoulumnTitile" style="width: 23%">Carrier</div>
+                  <div
+                    style="
+                      display: flex;
+                      flex-direction: column;
+                      border-right: 1px solid var(--color-system-border);
+                      width: 20%;
+                    "
+                  >
                     <div class="AicoulumnTitile2">Recommended Delivery Date</div>
-                    <div style="display: flex;height: 24px;align-items: center;">
-                      <div class="datetitle" style="border-right: 1px solid var(--color-system-border);">From (ETA/ATA + Days)</div>
+                    <div style="display: flex; height: 24px; align-items: center">
+                      <div
+                        class="datetitle"
+                        style="border-right: 1px solid var(--color-system-border)"
+                      >
+                        From (ETA/ATA + Days)
+                      </div>
                       <div class="datetitle">To (ETA/ATA + Days)</div>
                     </div>
                   </div>
-                  <div class="AirCoumlulnAdd" style="width: 10%;" @click="AddRuleItem(SeaContentList, 'Sea')">+ Add</div>
+                  <div
+                    class="AirCoumlulnAdd"
+                    style="width: 10%"
+                    @click="AddRuleItem(SeaContentList, 'Sea')"
+                  >
+                    + Add
+                  </div>
                 </div>
                 <div class="AirContent" v-for="(item, index) in SeaContentList" :key="index">
-                  <div class="AirCoumlumn" style="width: 10%;">{{ item.priority }}</div>
-                  <div class="AirCoumlumn" style="width: 14%;">
+                  <div class="AirCoumlumn" style="width: 10%">{{ item.priority }}</div>
+                  <div class="AirCoumlumn" style="width: 14%">
                     <el-select
                       v-model="item.rule_type"
                       disabled
-                      style="width: 100%;"
-                      @change="val => changeRuleType(val, index, SeaContentList)"
+                      style="width: 100%"
+                      @change="(val) => changeRuleType(val, index, SeaContentList)"
                     >
                       <el-option
                         v-for="opt in RuleTypeoptions"
@@ -497,45 +576,54 @@ const changeRuleType = (val: string, index: number, list: RuleItem[]) => {
                       />
                     </el-select>
                   </div>
-                  <div class="AirCoumlumn" style="width: 23%;">
+                  <div class="AirCoumlumn" style="width: 23%">
                     <SelectValue
                       ref="SeaPortRef"
                       :SelectIndex="index"
                       :SelectedValue="item.ports"
                       :typeisDisabled="item.rule_type"
                       SelectType="sea"
-                      @changeSelectedValue="val => changeSelectedValue(val, index, 'ports', SeaContentList)"
+                      @changeSelectedValue="
+                        (val) => changeSelectedValue(val, index, 'ports', SeaContentList)
+                      "
                     />
                   </div>
-                  <div class="AirCoumlumn" style="width: 23%;">
+                  <div class="AirCoumlumn" style="width: 23%">
                     <SelectValue
                       ref="SeaCarrierRef"
                       :SelectIndex="index"
                       :SelectedValue="item.carrier"
                       :typeisDisabled="item.rule_type"
                       SelectType="carrier"
-                      @changeSelectedValue="val => changeSelectedValue(val, index, 'carrier', SeaContentList)"
+                      @changeSelectedValue="
+                        (val) => changeSelectedValue(val, index, 'carrier', SeaContentList)
+                      "
                     />
                   </div>
-                  <div class="AirCoumlumn" style="width: 10%;">
-                    <el-input 
-                      @input="val => handleInput(val, index, 'recommended_delivery_from', SeaContentList)" 
-                      placeholder="Input" 
+                  <div class="AirCoumlumn" style="width: 10%">
+                    <el-input
+                      @input="
+                        (val) =>
+                          handleInput(val, index, 'recommended_delivery_from', SeaContentList)
+                      "
+                      placeholder="Input"
                       v-model="item.recommended_delivery_from"
                     />
                   </div>
-                  <div class="AirCoumlumn" style="width: 10%;">
-                    <el-input 
-                      @input="val => handleInput(val, index, 'recommended_delivery_to', SeaContentList)"  
-                      placeholder="Input" 
+                  <div class="AirCoumlumn" style="width: 10%">
+                    <el-input
+                      @input="
+                        (val) => handleInput(val, index, 'recommended_delivery_to', SeaContentList)
+                      "
+                      placeholder="Input"
                       v-model="item.recommended_delivery_to"
                     />
                   </div>
-                  <div class="AirDelete" style="width: 10%;">
-                    <el-button 
-                      v-if="item.rule_type !== '*Default Rule'" 
-                      @click="handleDelete(index, SeaContentList, 'Sea')" 
-                      class="el-button--blue" 
+                  <div class="AirDelete" style="width: 10%">
+                    <el-button
+                      v-if="item.rule_type !== '*Default Rule'"
+                      @click="handleDelete(index, SeaContentList, 'Sea')"
+                      class="el-button--blue"
                       style="height: 24px"
                     >
                       <span class="font_family icon-icon_delete_b"></span>
@@ -571,7 +659,7 @@ const changeRuleType = (val: string, index: number, list: RuleItem[]) => {
 :deep(.el-radio__input.is-checked + .el-radio__label) {
   color: var(--color-neutral-1);
 }
-:deep( .el-radio__inner) {
+:deep(.el-radio__inner) {
   border: 1px solid var(--color-system-checkbox-border);
 }
 :deep(.el-radio__inner) {
@@ -583,13 +671,17 @@ const changeRuleType = (val: string, index: number, list: RuleItem[]) => {
 }
 .oceanCheckbox {
   margin-bottom: 8px;
+  :deep(.el-checkbox__input) {
+    padding: 4px;
+  }
 }
 .delayedType {
   align-items: start;
   height: fit-content;
   margin-right: 5px;
   border-radius: 6px;
-  padding: 13px;
+  padding: 8px;
+  padding-left: 4px;
 }
 :deep(.el-checkbox) {
   width: 100%;
@@ -599,11 +691,14 @@ const changeRuleType = (val: string, index: number, list: RuleItem[]) => {
 }
 :deep(.el-checkbox__label) {
   width: 100%;
+  padding-left: 0px;
 }
 .titlecheckbox {
   width: 100%;
   display: flex;
   justify-content: space-between;
+  padding-left: 4px;
+  padding-top: 4px;
 }
 .icon_grey {
   color: #b8bbbf;
@@ -674,4 +769,4 @@ const changeRuleType = (val: string, index: number, list: RuleItem[]) => {
   align-items: center;
   justify-content: center;
 }
-</style>
+</style>

+ 16 - 15
src/views/DestinationDelivery/src/components/ConfiguRations/src/components/SelectValue.vue

@@ -35,16 +35,19 @@ const value = ref<string[]>(props.SelectedValue)
 const SelectNumber = ref(props.SelectIndex)
 const typeisDisabled = ref(props.typeisDisabled)
 const PortList = ref(props.PortList)
+let isUpdating = false
 
 watch(
   () => props.SelectedValue,
   (val) => {
+    if (isUpdating || JSON.stringify(val) === JSON.stringify(value.value)) return
+    isUpdating = true
     value.value = val
-    if(val[0] == 'ALL') {
-      checkAll.value = true
-    }
-  }
-, { immediate: true, deep: true })
+    checkAll.value = val[0] === 'ALL'
+    isUpdating = false
+  },
+  { immediate: true, deep: true }
+)
 
 watch(
   () => props.PortList,
@@ -113,17 +116,15 @@ const remoteMethod = (query: string) => {
 const emits = defineEmits(['changeSelectedValue', 'handelremovetag'])
 
 watch(value, (val) => {
-  if (val.length === 0) {
-    checkAll.value = false
-  } else if (val.length === PortList.value.length) {
-    checkAll.value = true
-    value.value = ['ALL']
-  }else if (val.length == 1 && val[0] == 'ALL') {
-    checkAll.value = true
-  } else {
-    checkAll.value = false
-  }
+  if (isUpdating) return
+  isUpdating = true
+  
+  const allChecked = val.length === PortList.value.length || 
+                    (val.length === 1 && val[0] === 'ALL')
+  checkAll.value = allChecked
+  
   emits('changeSelectedValue', val, SelectNumber.value)
+  isUpdating = false
 })
 
 const handleCheckAll = (val) => {

+ 287 - 164
src/views/DestinationDelivery/src/components/CreateNewBooking/src/CreateNewbooking.vue

@@ -1,37 +1,42 @@
 <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('')
+const deliveryDate = ref('')
+const DeliveryReference = ref('')
 const getAddressListData = ref({})
 // const isFocused = ref(false)
 const isFocused = ref({
   Shipper: false,
   Consignee: false,
-  Vessel: false
+  Vessel: false,
+  deliveryDate: false
 })
 const isataFocused = ref(false)
 const isetaFocused = ref(false)
@@ -49,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()
@@ -196,10 +198,6 @@ const showETAlabel = computed(() => {
   return ETATimeList.value != null || isetaFocused.value
 })
 
-const changeFocus = (val: boolean) => {
-  // isFocused.value = val
-}
-
 const changeFocustest = (type: any, val: boolean) => {
   isFocused.value[type] = val
 }
@@ -215,63 +213,56 @@ 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 getAddressCountryCityData = (type: any) => {
-  $api
-    .getAddressCountryCityData({
-      term: '',
-      term_type: type,
-      limit: type == 'country' ? CityCode.value : CountryCode.value
-    })
-    .then((res: any) => {
-      if (res.code === 200) {
-        if (type == 'country') {
-          countrys.value = res.data
-        } else {
-          city.value = res.data
-        }
-      }
-    })
-}
 const querySearchCountry = (query: string) => {
-  if (query) {
-    Countryloading.value = true
-    setTimeout(() => {
-      Countryloading.value = false
-      Countryoptions.value = countrys.value.filter((item) => {
-        return item.label.toLowerCase().includes(query.toLowerCase())
+  Countryloading.value = true
+  setTimeout(() => {
+    $api
+      .getAddressCountryCityData({
+        term: query,
+        term_type: 'country',
+        limit: CityCode.value != '' ? CityCode.value : ''
       })
-    }, 1000)
-  } else {
-    Countryoptions.value = []
-  }
+      .then((res: any) => {
+        if (res.code === 200) {
+          Countryoptions.value = res.data
+          Countryloading.value = false
+        }
+      })
+  }, 1000)
 }
 const querySearchCity = (query: string) => {
-  if (query) {
-    cityloading.value = true
-    setTimeout(() => {
-      cityloading.value = false
-      Cityoptions.value = city.value.filter((item) => {
-        return item.label.toLowerCase().includes(query.toLowerCase())
+  cityloading.value = true
+  setTimeout(() => {
+    $api
+      .getAddressCountryCityData({
+        term: query,
+        term_type: 'city',
+        limit: CountryCode.value != '' ? CountryCode.value : ''
       })
-    }, 1000)
-  } else {
-    Cityoptions.value = []
-  }
+      .then((res: any) => {
+        if (res.code === 200) {
+          Cityoptions.value = res.data
+          cityloading.value = false
+        }
+      })
+  }, 1000)
 }
 // 特殊日期样式
 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'
     }
   }
   // 默认样式
@@ -317,15 +308,15 @@ const AddNewAddressDelivery = () => {
 // 保存新地址
 const SaveNewAddress = () => {
   if (
-    AddressLine1.value != '' ||
-    AddressLine2.value != '' ||
-    AddressLine3.value != '' ||
-    (AddressLine4.value != '' &&
-      CountryCode.value != '' &&
+    (CountryCode.value != '' &&
       CityCode.value != '' &&
       PostalCode.value != '' &&
       ContactPerson.value != '' &&
-      ContactNumber.value != '')
+      ContactNumber.value != '' &&
+      AddressLine1.value != '') ||
+    AddressLine2.value != '' ||
+    AddressLine3.value != '' ||
+    AddressLine4.value != ''
   ) {
     const addressData = {
       address_1: AddressLine1.value,
@@ -368,20 +359,25 @@ const SaveNewAddress = () => {
     }
     AddNewAddressVisible.value = false
     currentEditAddress.value = null
+  } else {
+    ElMessage({
+      message: 'Required fields not entered.',
+      type: 'warning'
+    })
   }
 }
 // 点击按钮
 const handleclickbutton = (val: any) => {
-  Requirements.value = Requirements.value + val
+  Requirements.value = Requirements.value ? Requirements.value + ', ' + val : val
 }
 let delivery_address = ''
 const changeAddressRadio = () => {
   ManageVisible.value = false
   if (Addressradio.value != undefined) {
     isselectedAddress.value = Addressradio.value
+    selectedAddressList.value = ManageAddressList.value[Addressradio.value]
+    delivery_address = `${selectedAddressList.value.contact_person}(${selectedAddressList.value.contact_number})\n${selectedAddressList.value.address_1}\n${selectedAddressList.value.address_2}\n${selectedAddressList.value.address_3}\n${selectedAddressList.value.address_4},${selectedAddressList.value.country},${selectedAddressList.value.city},${selectedAddressList.value.postal_code}`
   }
-  selectedAddressList.value = ManageAddressList.value[Addressradio.value]
-  delivery_address = `${selectedAddressList.value.contact_person}(${selectedAddressList.value.contact_number})\n${selectedAddressList.value.address_1}\n${selectedAddressList.value.address_2}\n${selectedAddressList.value.address_3}\n${selectedAddressList.value.address_4},${selectedAddressList.value.country},${selectedAddressList.value.city},${selectedAddressList.value.postal_code}`
 }
 // 页面初始化
 let checkShipments = []
@@ -390,6 +386,7 @@ let checkShipmentsdata = []
 let checkShipmentsInfo = {}
 let checkShipmentsSubmitInfoData = {}
 const getInitBookingData = () => {
+  bookingTableRef.value.tableLoadingTable = true
   $api
     .InitCreateBooking({
       serial_no: a != undefined ? a : ''
@@ -411,6 +408,7 @@ const getInitBookingData = () => {
             DeliveryTime.value = res.data.data.delivery_time
             Modification.value = res.data.data.modify_reason
             selectedAddressList.value = res.data.data.delivery_address_detail
+            DeliveryReference.value = res.data.data.delivery_reference
             isselectedAddress.value = ''
             const sync_key = res.data.data.delivery_address_detail.sync_key
             checkShipments = res.data.data.tableData.map((item) => ({
@@ -457,6 +455,12 @@ const getInitBookingData = () => {
         }
       }
     })
+    .finally(() => {
+      bookingTableRef.value.tableLoadingTable = false
+    })
+}
+const clearManageDialog = () => {
+  ManageAddressList.value = []
 }
 // 查询Shipments
 const SearchShipment = () => {
@@ -465,6 +469,7 @@ const SearchShipment = () => {
     vessel: VesselNametest.value,
     consignee: ConsigneeValue.value,
     shipper: ShipperValue.value,
+    recommended_delivery_date: deliveryDate.value,
     eta_start: ETATimeList.value != null ? ETATimeList.value[0] : '',
     eta_end: ETATimeList.value != null ? ETATimeList.value[1] : '',
     ata_start: ATATimeList.value != null ? ATATimeList.value[0] : '',
@@ -522,6 +527,7 @@ const getDateRangeArray = (startDateStr, endDateStr) => {
   }
 }
 const selectChangeEvent = (val: any, date: any, submitInfo: any) => {
+  ManageAddressList.value = []
   getAddressListData.value = { ...val }
   checkShipmentsSubmitInfo.value = { ...submitInfo }
   if (date.length != 0) {
@@ -534,6 +540,7 @@ const selectChangeEvent = (val: any, date: any, submitInfo: any) => {
   }
 }
 
+const manageLoading = ref(false)
 // 点击 Address Book
 const handleClickAddress = () => {
   if (!isAddNewAddressVisible.value) {
@@ -543,6 +550,7 @@ const handleClickAddress = () => {
     (item) => item.contact_type !== 'Delete'
   )
   if (a == undefined) {
+    manageLoading.value = true
     $api
       .getAddressBookList({
         ...getAddressListData.value
@@ -556,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
@@ -616,7 +633,6 @@ const handleClickEditAddress = (val: any) => {
 // 删除地址
 const handleDeleteAddress = (val: any) => {
   const key = val.contact_type == 'Unedit' ? 'sync_key' : 'id'
-  console.log(val.contact_type)
   if (val.contact_type !== 'Add') {
     updateAddressList({ ...val, contact_type: 'Delete' })
   }
@@ -670,6 +686,7 @@ const SubmitBooking = () => {
       ...checkShipmentsSubmitInfoData,
       delivery_time: DeliveryTime.value,
       delivery_mode: modetypeValue.value,
+      delivery_reference: DeliveryReference.value,
       delivery_date: datetwo,
       delivery_address: delivery_address,
       special_requirements: Requirements.value,
@@ -689,6 +706,12 @@ const SubmitBooking = () => {
     })
 }
 
+const ctnsCount = computed(() => {
+  return bookingTableRef.value?.getTableCheckedRows().reduce((total, row) => {
+    return total + (Number(row.pakages) || 0)
+  }, 0)
+})
+
 onMounted(() => {
   getInitBookingData()
 })
@@ -700,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
         >
@@ -719,32 +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="flex">
+        <div class="top-filter-search">
           <el-input
             placeholder="Enter Booking/HBL/PO/Carrier Booking No. "
             v-model="CreateNewBOokingSearch"
-            style="width: 34%"
             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"
@@ -756,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"
@@ -768,14 +797,7 @@ onMounted(() => {
             </el-input>
             <span v-if="showLabelConsignee" class="border-label">Consignee</span>
           </div>
-          <el-button
-            style="width: 108px"
-            class="el-button--dark create-button"
-            @click="SearchShipment"
-            >Search</el-button
-          >
-        </div>
-        <div class="flex" style="margin-top: 8px">
+
           <div class="input-with-label">
             <CalendarDate
               :isNeedFooter="false"
@@ -786,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%"
@@ -795,7 +817,26 @@ onMounted(() => {
             ></CalendarDate>
             <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"
+              :showToday="false"
+              @focus="isFocused.deliveryDate = true"
+              @blur="isFocused.deliveryDate = false"
+              :format="userStore.dateFormat"
+              placeholder="Recommended Delivery Date"
+              valueFormat="MM/DD/YYYY"
+              style="width: 100%; height: 40px"
+            >
+            </a-date-picker>
+            <span v-if="isFocused.deliveryDate" class="border-label"
+              >Recommended Delivery Date</span
+            >
+          </div>
+
+          <div class="input-with-label">
             <!-- <AutoSelect ASPlaceholder="Input Vessel Name" :ASValue="VesselName" @changeFocus="changeFocus"></AutoSelect> -->
             <el-input
               placeholder="Input Vessel Name"
@@ -803,11 +844,19 @@ onMounted(() => {
               @focus="changeFocustest('Vessel', true)"
               @blur="changeFocustest('Vessel', false)"
               class="log_input"
+              style="width: 100%"
             >
             </el-input>
             <span v-if="showLabelVessel" class="border-label">Vessel Name</span>
           </div>
-          <div style="width: 108px"></div>
+          <div class="right-btn">
+            <el-button
+              style="width: 108px"
+              class="el-button--dark create-button"
+              @click="SearchShipment"
+              >Search</el-button
+            >
+          </div>
         </div>
       </div>
       <NewbookingTable
@@ -816,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>
@@ -907,8 +959,9 @@ onMounted(() => {
             :showToday="false"
             style="width: 240px"
             :format="userStore.dateFormat"
-            placeholder="Please Select Date"
             valueFormat="YYYY.MM.DD"
+            placeholder="Please Select Date"
+            class="delivery-date-picker"
           >
             <template #renderExtraFooter>
               <div class="recommended">
@@ -936,6 +989,17 @@ onMounted(() => {
             placeholder="Please Select Time"
           ></el-time-select>
         </div>
+        <div style="margin-left: 12px">
+          <div class="delivery_type_title">Delivery Reference</div>
+          <el-tooltip
+            class="item"
+            effect="dark"
+            content="Reference to be quoted on arrival at the Warehouse/DC"
+            placement="bottom"
+          >
+            <el-input v-model="DeliveryReference" class="delivery_reference"></el-input>
+          </el-tooltip>
+        </div>
       </div>
       <div class="delivery_type_title">Special Requirements</div>
       <div class="flex" style="margin-top: 8px">
@@ -985,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>
@@ -1148,7 +1232,6 @@ onMounted(() => {
             v-model="CountryCode"
             filterable
             remote
-            @focus="getAddressCountryCityData('country')"
             placeholder="Select Country"
             :remote-method="querySearchCountry"
             :loading="Countryloading"
@@ -1168,7 +1251,6 @@ onMounted(() => {
             v-model="CityCode"
             filterable
             remote
-            @focus="getAddressCountryCityData('city')"
             placeholder="Select City"
             :remote-method="querySearchCity"
             :loading="cityloading"
@@ -1289,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>
@@ -1314,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>
@@ -1345,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>
@@ -1376,6 +1446,10 @@ onMounted(() => {
   background-color: var(--color-mode);
   box-sizing: border-box;
 }
+.select-info {
+  font-size: 14px;
+  line-height: 40px;
+}
 .flex {
   display: flex;
 }
@@ -1394,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;
@@ -1426,7 +1500,14 @@ onMounted(() => {
   font-weight: 400;
 }
 .shipments_search {
-  margin: 0 0 8px 0;
+  display: flex;
+  margin-bottom: 16px;
+}
+.top-filter-search {
+  flex: 1;
+  display: grid;
+  grid-template-columns: repeat(4, 1fr);
+  grid-gap: 8px;
 }
 :deep(.log_input .el-input__wrapper) {
   box-shadow: 0 0 0 1px var(--color-select-border) inset;
@@ -1434,14 +1515,12 @@ onMounted(() => {
 }
 .input-with-label {
   position: relative;
-  display: inline-block;
-  width: 34%;
 }
 .border-label {
   position: absolute;
   top: -7px;
   left: 10px;
-  background: white; /* 用背景色覆盖边框 */
+  background: var(--color-mode); /* 用背景色覆盖边框 */
   padding: 0 5px;
   font-size: 12px;
   color: var(--color-neutral-2);
@@ -1455,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;
@@ -1486,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;
@@ -1548,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;
 }
@@ -1599,6 +1691,12 @@ onMounted(() => {
 :deep(.el-input__wrapper) {
   height: 40px;
 }
+.delivery_reference {
+  width: 240px;
+  :deep(.el-input__wrapper) {
+    height: 32px;
+  }
+}
 .inputmargin {
   margin: 4px 0 16px 0;
 }
@@ -1688,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>

+ 209 - 46
src/views/DestinationDelivery/src/components/CreateNewBooking/src/components/NewbookingTable.vue

@@ -4,6 +4,7 @@ import { useRowClickStyle } from '@/hooks/rowClickStyle'
 import { formatTimezone } from '@/utils/tools'
 import { ref, onMounted } from 'vue'
 import { useRouter } from 'vue-router'
+import { autoWidth } from '@/utils/table'
 
 const router = useRouter()
 const { currentRoute } = router
@@ -30,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)
@@ -48,7 +49,7 @@ const handleColumns = (columns: any) => {
         ...curColumn,
         formatter: ({ cellValue }: any) => formatTimezone(cellValue)
       }
-    }  else if (item.type === 'link') {
+    } else if (item.type === 'link') {
       curColumn = {
         ...curColumn,
         slots: { default: 'trackingNo' }
@@ -57,10 +58,16 @@ const handleColumns = (columns: any) => {
       curColumn = {
         ...curColumn,
         formatter: ({ cellValue }: any) => {
-          const array = cellValue.split("-")
-          return `${formatTimezone(array[0])} - ${formatTimezone(array[1])}`
+          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
   })
@@ -71,20 +78,23 @@ const getTableColumns = async () => {
   tableLoadingColumn.value = true
   await $api.BookingTableColumn().then((res: any) => {
     if (res.code === 200) {
-      if(a == undefined) {
+      if (a == undefined) {
         tableData.value.columns = [
           { type: 'checkbox', width: 50, fixed: 'left' },
           ...handleColumns(res.data.TrackingTableColumns)
         ]
-      }else {
-        tableData.value.columns = [
-          ...handleColumns(res.data.TrackingTableColumns)
-        ]
+      } else {
+        tableData.value.columns = [...handleColumns(res.data.TrackingTableColumns)]
       }
     }
   })
   nextTick(() => {
     tableLoadingColumn.value = false
+    tableRef.value &&
+      autoWidth(tableData.value, tableRef.value, {
+        packing_list: 190,
+        commercial_invoice: 180
+      })
   })
 }
 // 获取表格数据
@@ -96,15 +106,24 @@ const getTableData = (val: any) => {
 const searchTableData = (val: any) => {
   tableLoadingTable.value = true
   $api
-  .BookingTableSearch({
-    ...val
-  })
-  .then((res: any) => {
-    if (res.code === 200) {
+    .BookingTableSearch({
+      ...val
+    })
+    .then((res: any) => {
+      if (res.code === 200) {
+        tableData.value.data = res.data.data
+      }
+    })
+    .finally(() => {
       tableLoadingTable.value = false
-      tableData.value.data = res.data.data
-    }
-  })
+      nextTick(() => {
+        tableRef.value &&
+          autoWidth(tableData.value, tableRef.value, {
+            packing_list: 190,
+            commercial_invoice: 180
+          })
+      })
+    })
 }
 
 const emits = defineEmits(['selectChangeEvent'])
@@ -116,96 +135,209 @@ let checkShipmentsdata = []
 let checkShipmentsInfo = {}
 let checkShipmentsSubmitInfo = {}
 const checkUniformArray = (arrary: Array<{ consignee_id: any; country: any }>) => {
-  if (arrary.length === 0) return false;
-  const first = arrary[0];
+  if (arrary.length === 0) return false
+  const first = arrary[0]
   for (let i = 1; i < arrary.length; i++) {
     if (arrary[i].consignee_id !== first.consignee_id) {
-        return false;
+      return false
     }
   }
-  return [first];
+  return [first]
 }
-const selectChangeEvent = () => {
+
+// 将具有相同same_mbol的行选中或取消选中
+const selectRowsWithSameMbol = ({ row, checked }) => {
+  const key = row.same_mbol
+  if (!key) return
+  const tableRowData = tableRef.value?.getTableData().fullData || []
+  tableRowData.forEach((item, index) => {
+    if (item.same_mbol === key) {
+      tableRef.value?.setCheckboxRow(item, checked)
+    }
+  })
+}
+const selectChangeEvent = (selectItem) => {
+  selectRowsWithSameMbol(selectItem)
   const $grid = tableRef.value
   if ($grid) {
     const records = $grid.getCheckboxRecords()
-    checkShipments = records.map(item => ({ consignee_id: item.consignee_id, country: item.dc_country }))
-    checkRecommend = records.map(item => ({ date_range: item.date_range.split('-'), Hbol: item.h_bol  }))
+    checkShipments = records.map((item) => ({
+      consignee_id: item.consignee_id,
+      country: item.dc_country
+    }))
+    checkRecommend = records.map((item) => ({
+      date_range: item.date_range.split('-'),
+      Hbol: item.h_bol
+    }))
     const array = checkUniformArray(checkShipments)
-    if(array != false) {
+    if (array != false) {
       checkShipmentsdata = Object.keys(checkUniformArray(checkShipments)?.[0])
       checkShipmentsSubmit = Object.keys(records?.[0])
       checkShipmentsdata.forEach((item) => {
         Object.assign(checkShipmentsInfo, {
-          [item]: array.map((row) => row[item] )
+          [item]: array.map((row) => row[item])
         })
       })
       checkShipmentsSubmit.forEach((item) => {
         Object.assign(checkShipmentsSubmitInfo, {
           [item]: records.map((row) => {
-            if(row[item] == null){
+            if (row[item] == null) {
               return ''
             } else {
               return row[item]
             }
-          } )
+          })
         })
       })
     } else {
       checkShipmentsSubmitInfo = {}
       checkShipmentsInfo = {}
     }
-    emits('selectChangeEvent',checkShipmentsInfo, checkRecommend,checkShipmentsSubmitInfo)
+    emits('selectChangeEvent', checkShipmentsInfo, checkRecommend, checkShipmentsSubmitInfo)
   }
 }
 // 全选
-const selectAllChangeEvent= () => {
+const selectAllChangeEvent = () => {
   const $grid = tableRef.value
   if ($grid) {
     const records = $grid.getCheckboxRecords()
-    checkShipments = records.map(item => ({ consignee_id: item.consignee_id }))
-    checkRecommend = records.map(item => ({ date_range: item.date_range.split('-'), Hbol: item.h_bol }))
-    if(checkShipments.length != 0) {
+    checkShipments = records.map((item) => ({ consignee_id: item.consignee_id }))
+    checkRecommend = records.map((item) => ({
+      date_range: item.date_range.split('-'),
+      Hbol: item.h_bol
+    }))
+    if (checkShipments.length != 0) {
       checkShipmentsdata = Object.keys(checkShipments?.[0])
       checkShipmentsSubmit = Object.keys(records?.[0])
       checkShipmentsdata.forEach((item) => {
         Object.assign(checkShipmentsInfo, {
-          [item]: checkShipments.map((row) => row[item] )
+          [item]: checkShipments.map((row) => row[item])
         })
       })
       checkShipmentsSubmit.forEach((item) => {
         Object.assign(checkShipmentsSubmitInfo, {
           [item]: records.map((row) => {
-            if(row[item] == null){
+            if (row[item] == null) {
               return ''
             } else {
               return row[item]
             }
-          } )
+          })
         })
       })
     } else {
       checkShipmentsSubmitInfo = {}
     }
-    emits('selectChangeEvent',checkShipmentsInfo, checkRecommend,checkShipmentsSubmitInfo)
+    emits('selectChangeEvent', checkShipmentsInfo, checkRecommend, checkShipmentsSubmitInfo)
   }
 }
 
+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 CustomizeColumnsRef = ref()
+// 打开定制表格弹窗
+const handleCustomizeColumns = () => {
+  const params = {
+    getData: {
+      action: 'destination_delivery_booking',
+      operate: 'destination_delivery_shipment_display'
+    },
+    saveData: {
+      action: 'ajax',
+      operate: 'save_setting_display',
+      model_name: 'destination_delivery_shipment_search'
+    }
+  }
+  CustomizeColumnsRef.value.openDialog(
+    params,
+    -220,
+    'Drag item over to this selection or click "add" icon to show the field on delivery booking list'
+  )
+}
+// 定制表格
+const customizeColumns = async () => {
+  await getTableColumns()
+  nextTick(() => {
+    tableRef.value && autoWidth(tableData.value, tableRef.value)
+  })
+}
+
 // 实现行点击样式
 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="SettingTable">
+  <div class="new-booking-table">
+    <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' }"
@@ -214,22 +346,29 @@ defineExpose({
       @checkbox-change="selectChangeEvent"
       @checkbox-all="selectAllChangeEvent"
     >
+      <!-- download下载的插槽 -->
+      <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 v-if="isNotActivated" class="empty-text">
           This service isn't activated yet. Please contact our team to enable it.
         </div>
-        <div v-else class="empty-text">
-          No eligible shipments found to create a new booking.
-        </div>
+        <div v-else class="empty-text">No eligible shipments found to create a new booking.</div>
       </template>
     </vxe-grid>
+
+    <CustomizeColumns @customize="customizeColumns" ref="CustomizeColumnsRef" />
   </div>
 </template>
 
 <style lang="scss" scoped>
-.font_family::before {
-  color: var(--color-btn-danger-bg);
-}
 .icon_alert::before {
   color: var(--color-btn-warning-bg);
 }
@@ -257,4 +396,28 @@ defineExpose({
   color: var(--color-neutral-1);
   margin: 31px 0;
 }
+.download-btn {
+  cursor: pointer;
+
+  &:hover,
+  &:focus {
+    span,
+    .icon-style {
+      color: var(--color-theme) !important;
+    }
+  }
+}
+.new-booking-table {
+  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 - 276
src/views/DestinationDelivery/src/components/ModifyBooking/src/ModifyBooking.vue

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

+ 26 - 26
src/views/DestinationDelivery/src/components/TableView/src/TableView.vue

@@ -59,7 +59,7 @@ const handleColumns = (columns: any, status?: string) => {
           if (!cellValue) return '--'
           const rangeData = cellValue.split(';')
           const leftDate = rangeData[0] ? formatTimezone(rangeData[0]) : '_'
-          const rightDate = rangeData[1] ? formatTimezone(rangeData[0]) : '_'
+          const rightDate = rangeData[1] ? formatTimezone(rangeData[1]) : '_'
           return leftDate + ' -- ' + rightDate
         }
       }
@@ -88,7 +88,10 @@ const getTableColumns = async () => {
     }
   })
   nextTick(() => {
-    tableRef.value && autoWidth(tableData.value, tableRef.value)
+    tableRef.value &&
+      autoWidth(tableData.value, tableRef.value, {
+        status: 158
+      })
     tableLoadingColumn.value = false
     selectedTableData.value = []
   })
@@ -174,12 +177,15 @@ const getTableData = async (isPageChange?: boolean) => {
     .finally(() => {
       selectedTableData.value = []
       nextTick(() => {
-        tableRef.value && autoWidth(tableData.value, tableRef.value)
+        tableRef.value &&
+          autoWidth(tableData.value, tableRef.value, {
+            status: 158
+          })
         tableLoadingTableData.value = false
       })
     })
 }
-const SearchOperationLog = () => {
+const searchTableData = () => {
   tableLoadingTableData.value = true
   $api
     .getDeliveryBookingTableData({
@@ -198,15 +204,21 @@ const SearchOperationLog = () => {
     .finally(() => {
       selectedTableData.value = []
       nextTick(() => {
-        tableRef.value && autoWidth(tableData.value, tableRef.value)
+        tableRef.value &&
+          autoWidth(tableData.value, tableRef.value, {
+            status: 158
+          })
         tableLoadingTableData.value = false
       })
     })
 }
 onMounted(() => {
-  Promise.all([getTableColumns(), getTableData(false)]).finally(() => {
+  Promise.all([getTableColumns()]).finally(() => {
     nextTick(() => {
-      tableRef.value && autoWidth(tableData.value, tableRef.value)
+      tableRef.value &&
+        autoWidth(tableData.value, tableRef.value, {
+          status: 158
+        })
     })
   })
 })
@@ -273,7 +285,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']
@@ -301,7 +313,7 @@ const clickEmailBtn = (row: any) => {
 // edit
 const handleEdit = (row: any) => {
   router.push({
-    path: '/destination-delivery/CreateNewBooking',
+    path: '/destination-delivery/create-new-booking',
     query: { a: row._serial_no }
   })
 }
@@ -310,17 +322,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 +334,11 @@ const handleTips = (type: string, row: any) => {
   tipsDialogRef.value.openDialog(type, row)
 }
 const handleChangeRowState = () => {
-  SearchOperationLog()
+  searchTableData()
 }
 
 defineExpose({
-  SearchOperationLog,
+  searchTableData,
   isEmployeeRole
 })
 </script>
@@ -357,7 +358,6 @@ defineExpose({
       ref="tableRef"
       v-vloading="tableLoadingTableData || tableLoadingColumn"
       :height="props.height"
-      @cell-dblclick="({ row }) => handleEdit(row)"
       :style="{ border: 'none' }"
       v-bind="tableData"
     >
@@ -394,7 +394,7 @@ defineExpose({
           v-if="
             !isEmployeeRole &&
             row.status === 'Approved' &&
-            row.modify_by === userStore.userInfo.uname
+            row.create_by === userStore.userInfo.uname
           "
           @click="clickEmailBtn(row)"
           class="action-btn el-button--blue"
@@ -410,7 +410,7 @@ defineExpose({
           v-if="
             !isEmployeeRole &&
             (row.status === 'Pending Approval' || row.status === 'Rejected') &&
-            row.modify_by === userStore.userInfo.uname
+            row.create_by === userStore.userInfo.uname
           "
         >
           <span class="font_family icon-icon_edit_b"> </span>
@@ -423,7 +423,7 @@ defineExpose({
           v-if="
             !isEmployeeRole &&
             row.status === 'Pending Approval' &&
-            row.modify_by === userStore.userInfo.uname
+            row.create_by === userStore.userInfo.uname
           "
         >
           <span class="font_family icon-icon_cancelled_b"> </span>

+ 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>

Alguns arquivos não foram mostrados porque muitos arquivos mudaram nesse diff