ソースを参照

Merge branch 'dev_zyh' of United_Software/k_online_ui into dev

Jack Zhou 1 ヶ月 前
コミット
64a76f8eb1
85 ファイル変更8636 行追加537 行削除
  1. 1 0
      package.json
  2. BIN
      public/videos/demo-video.mp4
  3. 5 1
      src/App.vue
  4. 3 1
      src/api/index.ts
  5. 15 0
      src/api/module/Delivery.ts
  6. 0 1
      src/api/module/login.ts
  7. 282 0
      src/api/module/report.ts
  8. 37 0
      src/api/module/tracking.ts
  9. 2 4
      src/components/AutoComplete/src/AutoComplete.vue
  10. 9 10
      src/components/CustomizeColumns/src/CustomizeColumns.vue
  11. 0 4
      src/components/DateRange/src/components/CalendarDate.vue
  12. 1 2
      src/components/SelectTableSelect/src/SelectTableSelect.vue
  13. 183 4
      src/components/ShipmentStatus/src/ShipmentStatus.vue
  14. 0 1
      src/components/TransportMode/src/TransportMode.vue
  15. 1 0
      src/components/VEllipsisTooltip/index.ts
  16. 212 0
      src/components/VEllipsisTooltip/src/VEllipsisTooltip.vue
  17. 19 1
      src/components/VTag/src/VTag.vue
  18. 1 2
      src/components/selectAutoSelect/src/selectAutoSelect.vue
  19. 8 8
      src/hooks/calculatingHeight.ts
  20. 4 1
      src/main.ts
  21. 38 1
      src/router/index.ts
  22. 32 3
      src/stores/modules/breadCrumb.ts
  23. 14 0
      src/stores/modules/notificationMessage.ts
  24. 20 0
      src/stores/modules/trackingDownloadData.ts
  25. 65 4
      src/styles/Antdui.scss
  26. 16 6
      src/styles/elementui.scss
  27. 172 4
      src/styles/icons/iconfont.css
  28. 0 0
      src/styles/icons/iconfont.js
  29. 84 0
      src/styles/icons/iconfont.svg
  30. BIN
      src/styles/icons/iconfont.ttf
  31. BIN
      src/styles/icons/iconfont.woff
  32. BIN
      src/styles/icons/iconfont.woff2
  33. 4 0
      src/styles/theme-g.scss
  34. 18 1
      src/styles/theme.scss
  35. 4 1
      src/styles/vxeTable.scss
  36. 10 2
      src/utils/axios.ts
  37. 5 1
      src/utils/table.ts
  38. 1 1
      src/views/AIApiLog/src/components/LogDialog.vue
  39. 5 1
      src/views/Booking/src/components/BookingTable/src/BookingTable.vue
  40. 14 1
      src/views/Dashboard/src/DashboardView.vue
  41. 0 1
      src/views/Dashboard/src/components/RecentStatus.vue
  42. 221 190
      src/views/DestinationDelivery/src/components/ConfiguRations/src/components/CreateNewRule.vue
  43. 245 150
      src/views/DestinationDelivery/src/components/ConfiguRations/src/components/RecommendDate.vue
  44. 78 51
      src/views/DestinationDelivery/src/components/CreateNewBooking/src/CreateNewbooking.vue
  45. 187 45
      src/views/DestinationDelivery/src/components/CreateNewBooking/src/components/NewbookingTable.vue
  46. 10 1
      src/views/Layout/src/components/Header/HeaderView.vue
  47. 29 1
      src/views/Layout/src/components/Header/components/TrainingCard.vue
  48. 24 14
      src/views/Layout/src/components/Menu/MenuView.vue
  49. 1 0
      src/views/Report/index.ts
  50. 238 0
      src/views/Report/src/ReportView.vue
  51. 1 0
      src/views/Report/src/components/ReportDetail/index.ts
  52. 265 0
      src/views/Report/src/components/ReportDetail/src/ReportDetail.vue
  53. 314 0
      src/views/Report/src/components/ReportDetail/src/components/FieldsTable.vue
  54. 261 0
      src/views/Report/src/components/ReportDetail/src/components/ManageReportFields.vue
  55. BIN
      src/views/Report/src/components/ReportDetail/src/images/default_no_data@2x.png
  56. 1 0
      src/views/Report/src/components/ReportSchedule/index.ts
  57. 194 0
      src/views/Report/src/components/ReportSchedule/src/ReportSchedule.vue
  58. 507 0
      src/views/Report/src/components/ReportSchedule/src/components/EmailConfiguration.vue
  59. 253 0
      src/views/Report/src/components/ReportSchedule/src/components/FieldsTable.vue
  60. 261 0
      src/views/Report/src/components/ReportSchedule/src/components/ManageReportFields.vue
  61. 526 0
      src/views/Report/src/components/ReportSchedule/src/components/TimeRange.vue
  62. 178 0
      src/views/Report/src/components/ReportSchedule/src/components/ValidityPeriod.vue
  63. BIN
      src/views/Report/src/images/empty-dark.png
  64. BIN
      src/views/Report/src/images/empty-light.png
  65. 1 0
      src/views/TemplateManagement/index.ts
  66. 186 0
      src/views/TemplateManagement/src/TemplateManagement.vue
  67. 1 0
      src/views/TemplateManagement/src/components/CreateReportTemplate/index.ts
  68. 870 0
      src/views/TemplateManagement/src/components/CreateReportTemplate/src/CreateReportTemplate.vue
  69. 143 0
      src/views/TemplateManagement/src/components/CreateReportTemplate/src/components/AccountSelect.vue
  70. 792 0
      src/views/TemplateManagement/src/components/CreateReportTemplate/src/components/AdjustmentField.vue
  71. 185 0
      src/views/TemplateManagement/src/components/CreateReportTemplate/src/components/GroupNameSelect.vue
  72. 149 0
      src/views/TemplateManagement/src/components/CreateReportTemplate/src/components/PartyIDSelect.vue
  73. 1 0
      src/views/TemplateManagement/src/components/TableView/index.ts
  74. 457 0
      src/views/TemplateManagement/src/components/TableView/src/TableView.vue
  75. 1 0
      src/views/Tracking/src/components/DownloadAttachment/index.ts
  76. 616 0
      src/views/Tracking/src/components/DownloadAttachment/src/DownloadAttachment.vue
  77. BIN
      src/views/Tracking/src/components/DownloadAttachment/src/images/empty-img.png
  78. 0 1
      src/views/Tracking/src/components/TrackingDetail/src/TrackingDetail.vue
  79. 1 1
      src/views/Tracking/src/components/TrackingDetail/src/components/AttachmentView.vue
  80. 4 4
      src/views/Tracking/src/components/TrackingGuide.vue
  81. 128 12
      src/views/Tracking/src/components/TrackingTable/src/TrackingTable.vue
  82. BIN
      src/views/Tracking/src/image/dark-download-guide.png
  83. BIN
      src/views/Tracking/src/image/download-guide.png
  84. 1 0
      src/views/Video/index.ts
  85. 21 0
      src/views/Video/src/VideoView.vue

+ 1 - 0
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",

BIN
public/videos/demo-video.mp4


+ 5 - 1
src/App.vue

@@ -1,9 +1,13 @@
 <script setup lang="ts">
 import { RouterView } from 'vue-router'
+import { useRoute } from 'vue-router'
+import VideoView from '@/views/Video/src/VideoView.vue'
+const route = useRoute()
 </script>
 
 <template>
-  <RouterView />
+  <VideoView v-if="route.name === 'Demo Video'" />
+  <RouterView v-else />
 </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 // 取出所有可遍历属性赋值在新的对象上

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

@@ -318,4 +318,19 @@ 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
+  )
 }

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

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

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

@@ -0,0 +1,282 @@
+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 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
+  )
+}

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

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

+ 9 - 10
src/components/CustomizeColumns/src/CustomizeColumns.vue

@@ -166,10 +166,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 +235,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 +274,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 +363,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">

+ 0 - 4
src/components/DateRange/src/components/CalendarDate.vue

@@ -128,10 +128,6 @@ const handlePanelChange = (value: any, mode: any) => {
     isShowExtra.value = true
   }
 }
-// 判断失焦时是否两个都有值
-const isTwoDate = (date: any) => {
-  console.log(date)
-}
 </script>
 <template>
   <div>

+ 1 - 2
src/components/SelectTableSelect/src/SelectTableSelect.vue

@@ -84,7 +84,7 @@ const changeAutoSelect = (val: any, value: any) => {
   }
   AutoSelectObj[val] = value.join()
   AutoSelectObj2[val] = value
-  emit('changeAutoSelect', AutoSelectObj, AutoSelectObj2,errorBoolean.value)
+  emit('changeAutoSelect', AutoSelectObj, AutoSelectObj2, errorBoolean.value)
 }
 const typeSelectFocus = (index: any, e: any) => {
   typeSelectIndex.value = index
@@ -157,7 +157,6 @@ const checkdestination = (row: any, index: any) => {
         v-model="AddType[index].placesType"
         :suffix-icon="IconDropDown"
         @blur="typeSelectBlur"
-        class="testname"
         @focus="typeSelectFocus(index, $event)"
         @click="typeSelectClick(index, $event)"
         @change="changeSelect(AddType[index].placesType)"

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

+ 0 - 1
src/components/TransportMode/src/TransportMode.vue

@@ -19,7 +19,6 @@ const TransportList = ref(props.TransportListItem)
 watch(
   () => props.TransportListItem,
   (current) => {
-    console.log(current)
     TransportList.value = current
     TransportList.value.forEach((item: any) => {
       if (item.checked) {

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

+ 19 - 1
src/components/VTag/src/VTag.vue

@@ -13,6 +13,8 @@ interface internalProps {
     | 'Pending Approval'
     | 'Approved'
     | 'Rejected'
+    | 'Active'
+    | 'Inactive'
   large?: boolean
 }
 
@@ -28,7 +30,9 @@ const mappingTable = new Map([
   ['Arrived', 'arrived'],
   ['Completed', 'completed'],
   ['Departed', 'Departed'],
-  ['Pending Approval', 'pending-approval']
+  ['Pending Approval', 'pending-approval'],
+  ['Active', 'active'],
+  ['Inactive', 'inactive']
 ])
 defineProps<internalProps>()
 </script>
@@ -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);

+ 1 - 2
src/components/selectAutoSelect/src/selectAutoSelect.vue

@@ -196,7 +196,6 @@ const typeSelectClick = (index: any, val: any) => {
         v-model="AddType[index].partyType"
         :suffix-icon="IconDropDown"
         @blur="typeSelectBlur"
-        class="testname"
         @focus="typeSelectFocus(index, $event)"
         @click="typeSelectClick(index, $event)"
         @change="changeSelect(AddType[index].partyType)"
@@ -312,4 +311,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 - 1
src/main.ts

@@ -22,6 +22,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)
 
@@ -35,7 +36,9 @@ VXETable.use(VXETablePluginExportXLSX, {
 VXETable.setI18n('en-US', enUS)
 VXETable.setLanguage('en-US')
 
-app.use(createPinia())
+const pinia = createPinia()
+pinia.use(piniaPluginPersistedstate)
+app.use(pinia)
 app.use(VXETable)
 app.use(VxeUI)
 app.use(router)

+ 38 - 1
src/router/index.ts

@@ -21,6 +21,27 @@ const router = createRouter({
           name: 'Booking',
           component: () => import('../views/Booking')
         },
+        {
+          path: '/report',
+          name: 'Report Management',
+          component: () => import('../views/Report')
+        },
+        {
+          path: '/report/detail',
+          name: 'ReportDetail',
+          component: () => import('../views/Report/src/components/ReportDetail'),
+          meta: {
+            activeMenu: '/report'
+          }
+        },
+        {
+          path: '/report/schedule',
+          name: 'ReportSchedule',
+          component: () => import('../views/Report/src/components/ReportSchedule'),
+          meta: {
+            activeMenu: '/report'
+          }
+        },
         {
           path: '/booking/detail',
           name: 'Booking Detail',
@@ -111,6 +132,19 @@ const router = createRouter({
           name: 'PromptConfiguration',
           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',
@@ -161,7 +195,10 @@ const router = createRouter({
         {
           path: '/destination-delivery/ConfiguRations/CreateNewRule',
           name: 'Destination Create New Rule',
-          component: () => import('../views/DestinationDelivery/src/components/ConfiguRations/src/components/CreateNewRule.vue')
+          component: () =>
+            import(
+              '../views/DestinationDelivery/src/components/ConfiguRations/src/components/CreateNewRule.vue'
+            )
         },
         {
           path: '/destination-delivery/modify-booking',

+ 32 - 3
src/stores/modules/breadCrumb.ts

@@ -12,6 +12,7 @@ interface BreadCrumb {
 const whiteList = [
   'Booking Detail',
   'Tracking Detail',
+  'Tracking Download Attachment',
   'Add VGM',
   'Public Tracking Detail',
   'Create New Rule',
@@ -19,6 +20,7 @@ const whiteList = [
   'Configurations',
   'Create New Booking',
   'Destination Create New Rule',
+  "Create Report Template",
 ]
 
 export const useBreadCrumb = defineStore('breadCrumb', {
@@ -99,7 +101,7 @@ export const useBreadCrumb = defineStore('breadCrumb', {
         ]
       } else if (toRoute.name === 'Destination Create New Rule') {
         let label = ''
-        if(toRoute.query.a != undefined) {
+        if (toRoute.query.a != undefined) {
           label = 'Modify Rule'
         } else {
           label = 'Create New Rule'
@@ -121,8 +123,9 @@ export const useBreadCrumb = defineStore('breadCrumb', {
             query: toRoute.query
           }
         ]
-      } else if (toRoute.name === 'Create New Booking') {let label = ''
-        if(toRoute.query.a != undefined) {
+      } else if (toRoute.name === 'Create New Booking') {
+        let label = ''
+        if (toRoute.query.a != undefined) {
           label = 'Modify Booking'
         } else {
           label = 'Create New Booking'
@@ -139,6 +142,32 @@ export const useBreadCrumb = defineStore('breadCrumb', {
             query: toRoute.query
           }
         ]
+      } else if (toRoute.name === 'ReportDetail') {
+        this.routeList = [
+          {
+            label: 'Report Management',
+            path: '/report',
+            query: ''
+          },
+          {
+            label: 'Details',
+            path: '/report/detail',
+            query: toRoute.query
+          }
+        ]
+      } else if (toRoute.name === 'ReportSchedule') {
+        this.routeList = [
+          {
+            label: 'Report Management',
+            path: '/report',
+            query: ''
+          },
+          {
+            label: 'Schedule Configuration',
+            path: '/report/detail',
+            query: toRoute.query
+          }
+        ]
       } else if (toRoute.name && whiteList.includes(toRoute.name)) {
         this.routeList.push({
           label: toRoute?.meta?.breadName || toRoute.name,

+ 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 // 永久保留
+  }
+});

+ 65 - 4
src/styles/Antdui.scss

@@ -167,9 +167,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);
 }
@@ -200,6 +198,16 @@ tr
   height: 32px !important;
   border-radius: 6px !important;
 }
+:where(.css-dev-only-do-not-override-1p3hq3p).ant-picker-dropdown  div.ant-picker-cell-inner{
+  color: var(--color-el-date-prev);
+}
+
+:where(.css-dev-only-do-not-override-1p3hq3p).ant-picker-dropdown .ant-picker-cell-in-view .ant-picker-cell-inner{
+  color: var(--color-neutral-1);
+}
+// :not(.ant-picker-cell-in-view).ant-picker-dropdown .ant-picker-cell .ant-picker-cell-inner {
+//   color: var(--color-el-date-prev);
+// }
 .ant-checkbox-checked .ant-checkbox-inner {
   background-color: var(--color-theme) !important;
   border-color: var(--color-theme) !important;
@@ -209,4 +217,57 @@ 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;
+  }
+}
+
+:where(.css-dev-only-do-not-override-1p3hq3p).ant-select-dropdown {
+  background-color: var(--management-bg-color);
+}
+:where(.css-dev-only-do-not-override-1p3hq3p).ant-select:not(.ant-select-customize-input) div.ant-select-selector {
+  background-color: var(--management-bg-color);
+}
+
+ :where(.css-dev-only-do-not-override-1p3hq3p).ant-select-single.ant-select-open .ant-select-selection-item {
+    color: var(--color-neutral-1);
+ }
+ :where(.css-dev-only-do-not-override-1p3hq3p).ant-select:not(.ant-select-customize-input) .ant-select-selector {
+  border-color: var(--color-select-border);
+ }
+
+ .rc-virtual-list-scrollbar-thumb {
+  background-color: var(--color-scrollbar-thumb) !important;
+ }

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

+ 172 - 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=1764126605090') format('woff2'),
+       url('iconfont.woff?t=1764126605090') format('woff'),
+       url('iconfont.ttf?t=1764126605090') format('truetype'),
+       url('iconfont.svg?t=1764126605090#font_family') format('svg');
 }
 
 .font_family {
@@ -14,6 +14,174 @@
   -moz-osx-font-smoothing: grayscale;
 }
 
+.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";
 }

ファイルの差分が大きいため隠しています
+ 0 - 0
src/styles/icons/iconfont.js


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

@@ -14,6 +14,90 @@
     />
       <missing-glyph />
       
+      <glyph glyph-name="icon_active" unicode="&#59204;" d="M512 896a512 512 0 1 0 0-1024A512 512 0 0 0 512 896z m0-87.771429a424.228571 424.228571 0 1 1 0-848.457142A424.228571 424.228571 0 0 1 512 808.228571z m287.012571-290.962285l-359.570285-359.643429-31.085715 31.012572-183.369142 183.442285 31.012571 31.012572 31.012571 31.085714 152.356572-152.356571 297.691428 297.472 61.952-62.025143z"  horiz-adv-x="1024" />
+      
+      <glyph glyph-name="icon_video_b" unicode="&#59203;" d="M871.104 818.24c47.744 0 86.4-38.656 86.4-86.4v-734.08a86.4 86.4 0 0 0-86.4-86.4H137.024a86.4 86.4 0 0 0-86.4 86.4V731.84c0 47.744 38.656 86.4 86.4 86.4h734.08z m-743.68-820.48c0-5.312 4.288-9.6 9.6-9.6h734.08a9.6 9.6 0 0 1 9.6 9.6V503.616H127.36v-505.856z m276.032 397.312a16 16 0 0 0 24 13.888l256.704-148.288a16 16 0 0 0 0-27.648L427.52 84.736a16 16 0 0 0-24 13.824V395.072zM136.96 741.504a9.6 9.6 0 0 1-9.6-9.6v-151.552h144l-12.608 36.992a22548.48 22548.48 0 0 0-41.792 124.16H136.96z m194.56-99.456l20.992-61.696h139.52l-12.608 36.992c-13.696 40.192-29.376 86.912-41.792 124.16H297.984c10.752-31.808 22.72-67.648 33.6-99.456z m220.672 0l20.992-61.696h139.52l-12.608 36.992c-13.632 40.192-29.376 86.912-41.792 124.16H518.656c10.752-31.872 22.784-67.648 33.6-99.456z m220.672 0l20.992-61.696h86.784V731.904a9.6 9.6 0 0 1-9.6 9.6h-131.712c10.688-31.872 22.72-67.648 33.536-99.456z"  horiz-adv-x="1024" />
+      
+      <glyph glyph-name="icon_filters_up_b" unicode="&#59202;" d="M267.648 79.68L512 324.032l244.416-244.352 54.272 54.336-271.552 271.488a38.4 38.4 0 0 1-54.272 0l-271.552-271.488 54.336-54.336z m0 271.488L512 595.584l244.416-244.416 54.272 54.336-271.552 271.488a38.4 38.4 0 0 1-54.272 0L213.312 405.504l54.336-54.336z"  horiz-adv-x="1024" />
+      
+      <glyph glyph-name="icon_charge_trucking" unicode="&#59201;" d="M629.12 706.944a32 32 0 0 0 32-32v-63.2h158.4a32 32 0 0 0 22.304-9.056l2.432-2.624 140.48-171.168c4.672-5.696 7.264-12.896 7.264-20.288v-297.408a32 32 0 0 0-32-32h-101.12a120.448 120.448 0 0 0-227.968-1.184l-1.792-0.064h-252.8a120.448 120.448 0 0 0-227.52 0H64a32 32 0 0 0-32 32V674.912a32 32 0 0 0 32 32h565.12zM262.56 173.952a56.448 56.448 0 1 1 0-112.896 56.448 56.448 0 0 1 0 112.864z m482.176 0a56.448 56.448 0 1 1 0-112.896 56.448 56.448 0 0 1 0 112.864zM96 141.952h48.608a120.48 120.48 0 0 0 235.936 0h215.904V579.744c0 2.24 0.224 4.448 0.672 6.56v56.64H96v-500.992z m565.12 62.208a120.48 120.48 0 0 0 201.28-60.96H928V397.12l-123.616 150.592H661.12V204.16z"  horiz-adv-x="1024" />
+      
+      <glyph glyph-name="icon_charge_hubrouting" unicode="&#59194;" d="M512 820.16a112.192 112.192 0 0 0 32-219.68v-67.968a184.8 184.8 0 0 0 133.344-99.712l90.592 30.848a112.192 112.192 0 1 0 22.56-59.904l-95.104-32.384a184 184 0 0 0-53.76-152l58.944-56.224a112.192 112.192 0 1 0-49.408-41.312l-63.424 60.512A183.872 183.872 0 0 0 512 166.08a183.936 183.936 0 0 0-75.776 16.32l-68.576-65.472a112.192 112.192 0 1 0-46.944 43.648l61.568 58.816a184.032 184.032 0 0 0-53.44 154.208l-93.984 32a112.192 112.192 0 1 0 21.376 60.32l91.392-31.104A184.8 184.8 0 0 0 480 532.48V600.48a112.224 112.224 0 0 0 32 219.712zM271.008 108.16a48.192 48.192 0 1 1 0-96.352 48.192 48.192 0 0 1 0 96.384z m473.792 0a48.192 48.192 0 1 1 0-96.352 48.192 48.192 0 0 1 0 96.384zM512 471.36a120.64 120.64 0 1 1 0-241.28 120.64 120.64 0 0 1 0 241.28zM144.192 519.808a48.192 48.192 0 1 1 0-96.384 48.192 48.192 0 0 1 0 96.384z m735.616 0a48.192 48.192 0 1 1 0-96.384 48.192 48.192 0 0 1 0 96.384zM512 756.192a48.192 48.192 0 1 1 0-96.384 48.192 48.192 0 0 1 0 96.384z"  horiz-adv-x="1024" />
+      
+      <glyph glyph-name="icon_charge_gatewayhandling" unicode="&#59195;" d="M967.008 846.88a32 32 0 0 0 32-32v-621.184a32 32 0 0 0-32-32H825.28v-232.48a32 32 0 0 0-32-32H71.008a32 32 0 0 0-32 32V481.984a32 32 0 0 0 32 32h248.16l3.136-0.128a32 32 0 0 0 19.488-9.216l64-64.032V814.88a32 32 0 0 0 32 32H967.04z m-864-885.664H761.28V336.992H432.16a32 32 0 0 0-22.624 9.376L305.92 449.984H103.008v-488.768z m366.816 439.776H793.28a32 32 0 0 0 32-32v-143.296h109.696V782.88H469.824V400.96z m363.84 84.384h-276.928v64h276.928v-64z m0 125.376h-276.928v64h276.928v-64z"  horiz-adv-x="1024" />
+      
+      <glyph glyph-name="icon_charge_airfreight" unicode="&#59196;" d="M513.568 864c55.264 0 100.064-44.8 100.064-100.064v-233.568l338.656-211.712a32 32 0 0 0 15.04-27.136v-88.416a32 32 0 0 0-41.632-30.496l-312.064 98.56v-185.28l76.288-58.208a32 32 0 0 0 12.576-25.44v-66.304a32 32 0 0 0-40.96-30.72l-148.192 43.072-147.52-43.072a32 32 0 0 0-40.96 30.72V2.24a32 32 0 0 0 12.736 25.536l75.904 57.376v187.008l-315.2-99.552a32 32 0 0 0-41.6 30.496V291.52a32 32 0 0 0 15.04 27.136l341.76 213.664V763.936C413.504 819.2 458.304 864 513.568 864z m0-64c-19.904 0-36.064-16.16-36.064-36.064v-249.376a32 32 0 0 0-15.04-27.136l-341.76-213.6v-27.072l315.2 99.52a32 32 0 0 0 41.6-30.496v-246.56a32 32 0 0 0-12.736-25.536l-75.904-57.344v-7.712l115.488 33.696c5.824 1.696 12.064 1.696 17.92 0l116.224-33.76v7.872l-73.504 56a31.936 31.936 0 0 0-15.36 27.328v245.024a32 32 0 0 0 41.6 30.496l312.096-98.56v27.104l-338.688 211.68a32 32 0 0 0-15.04 27.136V763.936c0 19.936-16.128 36.064-36.032 36.064z"  horiz-adv-x="1024" />
+      
+      <glyph glyph-name="icon_charge_localhandling" unicode="&#59197;" d="M434.304 815.488a32 32 0 0 0 23.584-1.312l386.144-179.392a32 32 0 0 0 11.264-8.32c0.224-0.192 0.384-0.416 0.576-0.64a30.656 30.656 0 0 0 1.984-2.688l0.64-0.992c0.576-0.96 1.12-1.92 1.6-2.912l0.384-0.896c0.48-1.056 0.864-2.144 1.216-3.232l0.288-0.896c0.32-1.152 0.544-2.304 0.736-3.456l0.192-0.896c0.192-1.44 0.32-2.88 0.32-4.352v-175.392h2.688a32 32 0 0 0 27.712-16l104-180.16a32 32 0 0 0 0-32l-104-180.16c-5.696-9.856-16.288-16-27.712-16l-207.968 0.064a32 32 0 0 0-27.712 16L624.32 32l-10.88-5.024-159.2-73.28a31.904 31.904 0 0 0-6.176-1.984c-0.448-0.096-0.896-0.256-1.376-0.32l-1.44-0.192c-2.176-0.32-4.384-0.48-6.592-0.32l-0.352 0.064a31.808 31.808 0 0 0-11.008 2.848L42.72 133.376a32 32 0 0 0-18.464 28.992V594.176a31.808 31.808 0 0 0 16.448 40.448L431.04 814.208l3.264 1.28z m38.592-410.112v-372.64l113.792 52.352 5.536 2.56-65.984 114.336a32 32 0 0 0 0 32l103.968 180.128 2.368 3.52a32 32 0 0 0 25.344 12.48h141.312V555.52l-326.336-150.144z m-384.64-222.624l320.608-149.696V405.472L88.256 554.4v-371.648z m502.624 35.2l85.504-148.096h171.072l85.504 148.128-85.504 148.128h-171.072l-85.504-148.16z m122.464 84.16a97.216 97.216 0 1 0 97.216-168.416 97.216 97.216 0 0 0-97.216 168.416z m77.344-67.584a33.184 33.184 0 1 1-57.472-33.248 33.184 33.184 0 0 1 57.472 33.28zM131.2 605.856l309.728-144.64 131.84 60.672-289.664 153.856L131.2 605.856z m224.992 103.488l289.728-153.824 108.96 50.112-310.56 144.256-88.128-40.544z"  horiz-adv-x="1024" />
+      
+      <glyph glyph-name="icon_charge_customs" unicode="&#59198;" d="M501.056 862.848a32 32 0 0 0 19.648-0.832L873.6 731.456a32 32 0 0 0 20.896-29.984v-404.48c0-21.952-7.04-44.032-17.056-64.64-10.112-20.864-24.192-42.176-40.384-63.04-32.384-41.792-75.36-84.576-118.816-122.784a1324.544 1324.544 0 0 0-125.344-97.792c-18.368-12.48-35.2-22.976-49.184-30.432a186.944 186.944 0 0 0-19.84-9.408 63.2 63.2 0 0 0-22.336-4.896c-7.936 0-15.584 2.24-20.864 4.032a179.104 179.104 0 0 0-19.232 8.128c-13.472 6.528-29.6 15.68-47.168 26.784a982.048 982.048 0 0 0-118.976 89.696c-41.184 36.16-81.92 78.24-112.608 122.368-30.304 43.584-53.184 92.608-53.184 141.952V701.472a32 32 0 0 0 20.736 29.952l348.096 130.56 2.72 0.864zM193.504 679.36V296.96c0-30.4 14.592-66.4 41.76-105.472 26.752-38.464 63.456-76.704 102.272-110.816a918.112 918.112 0 0 1 111.008-83.712c16.192-10.24 30.08-18.08 40.704-23.2 5.056-2.432 8.992-4.064 11.776-5.024 2.912 1.152 7.072 3.04 12.48 5.92 11.296 6.08 26.112 15.2 43.424 26.976 34.56 23.488 77.44 56.32 119.072 92.928 41.76 36.736 81.472 76.544 110.464 113.92 14.496 18.688 25.792 36.16 33.376 51.744 7.68 15.84 10.656 28.064 10.656 36.736V679.2L509.664 797.888l-316.16-118.56zM511.936 640a102.912 102.912 0 0 0 32-200.672v-61.184h64.288v-64h-64.32v-122.56c35.52 7.84 62.304 27.744 82.432 52.16 20.448 24.768 33.536 53.696 40.64 77.376h66.112c-7.52-33.728-25.28-79.232-57.376-118.112-35.68-43.264-89.792-79.04-165.536-79.04-75.648 0-130.304 35.712-166.784 78.72-33.024 38.976-51.808 84.704-60 118.432h66.496c7.712-23.616 21.44-52.384 42.368-77.056 21.536-25.44 50.048-45.952 87.68-53.216v123.296h-64.288v64h64.256V439.36A102.912 102.912 0 0 0 511.936 640z m0-64a38.912 38.912 0 1 1 0-77.824 38.912 38.912 0 0 1 0 77.824z"  horiz-adv-x="1024" />
+      
+      <glyph glyph-name="icon_charge_terminalcharge" unicode="&#59199;" d="M992 615.04v-645.312h-64V570.752l-422.496 159.04L96 571.008v-601.28H32V614.816L493.76 793.92l11.392 4.384L992 615.04zM454.464-30.208h-123.04V92.8h123.04v-123.008z m283.584 0h-123.04V92.8h123.04v-123.008z m-139.136 122.976H475.84v123.04h123.04V92.8z"  horiz-adv-x="1024" />
+      
+      <glyph glyph-name="icon_charge_airfreightrouting" unicode="&#59200;" d="M845.664 799.68A64 64 0 0 0 903.136 736v-155.776l-0.352-6.528a64 64 0 0 0-57.12-57.12l-6.528-0.352H455.232l-6.56 0.32a64 64 0 0 0-57.12 57.152l-0.32 6.528V627.424H275.808a106.08 106.08 0 0 1 0-212.16H748.8a169.504 169.504 0 0 0 0-338.976h-119.136v-44.32l-0.352-6.528a64 64 0 0 0-57.12-57.12l-6.528-0.352H181.728l-6.528 0.32a64 64 0 0 0-57.152 57.152l-0.32 6.528v155.776a64 64 0 0 0 57.472 63.68l6.528 0.32h383.904l6.528-0.32a64 64 0 0 0 57.472-63.68v-47.456h119.136a105.472 105.472 0 1 1 0 210.976H275.84a170.08 170.08 0 1 0 0 340.16h115.424V736a64 64 0 0 0 57.44 63.68l6.56 0.32h383.904l6.528-0.32zM181.728 31.936h383.904v155.776H181.728V32z m273.504 548.256h383.904V736H455.232v-155.776z"  horiz-adv-x="1024" />
+      
+      <glyph glyph-name="icon_barcode_b" unicode="&#59191;" d="M198.848 61.312h-86.4V706.624h86.4v-645.312z m151.296 0H281.152V706.624H350.08v-645.312z m160.896 0H407.424V706.624h103.68v-645.312z m122.112 0H564.096V706.624h69.056v-645.312z m123.072 0h-68.992V706.624h68.992v-645.312z m155.264 0h-103.616V706.624h103.68v-645.312z"  horiz-adv-x="1024" />
+      
+      <glyph glyph-name="icon_booking__fill_b1" unicode="&#59192;" d="M1000.32 2.56a96 96 0 0 0-96-96h-768a96 96 0 0 0-96 96V489.344h960v-486.784z m-539.072 133.76L339.328 258.176l-27.136 27.2-54.336-54.336 27.2-27.072 149.12-149.12a38.4 38.4 0 0 1 54.272 0l294.272 294.272-54.208 54.336-267.264-267.136zM312 791.68h432.896V861.504h76.736v-69.952h82.752a96 96 0 0 0 96-96v-131.968h-960V695.616a96 96 0 0 0 96 96h98.88V861.568h76.736v-69.952z"  horiz-adv-x="1024" />
+      
+      <glyph glyph-name="icon_customer_b" unicode="&#59193;" d="M663.744 277.312c65.536-25.024 196.672-83.2 236.032-131.328 40.32-49.344 56.96-121.536 60.288-151.424H64c1.472 30.016 15.168 102.272 58.24 151.424 41.792 47.808 168 106.112 230.72 131.264a6.4 6.4 0 0 0 7.872-2.752L460.16 102.4a6.4 6.4 0 0 1 11.52 0.832l12.608 31.552a6.4 6.4 0 0 1-0.384 5.568l-28.352 49.088a6.4 6.4 0 0 0 1.024 7.744l48.768 48.832a6.4 6.4 0 0 0 9.088 0l49.408-49.024a6.4 6.4 0 0 0 1.28-7.424l-24.96-49.472a6.464 6.464 0 0 1-0.32-4.992l10.496-30.464a6.4 6.4 0 0 1 11.648-1.024l93.952 170.88a6.4 6.4 0 0 0 7.808 2.88z m-151.68 496.064a216.576 216.576 0 1 0 0-433.152 216.576 216.576 0 0 0 0 433.152z"  horiz-adv-x="1024" />
+      
+      <glyph glyph-name="icon_warning_b" unicode="&#59181;" d="M910.976-115.2H113.024v76.8h797.952v-76.8zM512 635.392a297.664 297.664 0 0 0 297.664-297.728v-295.424H214.4V337.664A297.728 297.728 0 0 0 512 635.392z m0-76.8a220.864 220.864 0 0 1-220.864-220.928v-218.624h139.776v135.168h76.8v-135.168h225.152V337.664A220.864 220.864 0 0 1 512 558.592z m-327.232-11.968l-39.872-65.536L19.648 557.184 59.52 622.72l125.248-76.096z m819.648 10.56l-125.248-76.16-39.872 65.6 125.248 76.16 39.872-65.6zM551.808 736.64h-76.8V883.2h76.8v-146.56z"  horiz-adv-x="1024" />
+      
+      <glyph glyph-name="icon_update_b" unicode="&#59182;" d="M751.68 400.064h87.424a333.12 333.12 0 0 1-560.96 224l-26.304 27.968-26.432 27.968a409.792 409.792 0 0 0 690.496-279.936h88.512L878.08 274.944 751.68 400z m-234.24-423.68A409.792 409.792 0 0 0 108.032 368H19.584L145.92 493.056 272.32 368H184.896a333.12 333.12 0 0 1 560.96-224.064l26.304-27.904 26.368-27.968a408.64 408.64 0 0 0-281.088-111.552z"  horiz-adv-x="1024" />
+      
+      <glyph glyph-name="icon_purchase__order_b" unicode="&#59183;" d="M388.544 112.512a63.36 63.36 0 1 0 0-126.848 63.36 63.36 0 0 0 0 126.848z m440.448 0a63.36 63.36 0 1 0 0-126.848 63.36 63.36 0 0 0 0 126.848z m-562.56 704a38.4 38.4 0 0 0 30.848-28.544l27.328-110.272h516.8a96 96 0 0 0 92.928-120.128l-66.816-256.96a96 96 0 0 0-92.864-71.872H411.52l-22.4-29.44h503.232a38.4 38.4 0 1 0 0-76.8h-580.48a38.464 38.464 0 0 0-30.592 61.568l65.472 86.528-116.8 469.76H133.504a38.4 38.4 0 1 0 0 76.736h126.528l6.4-0.512z"  horiz-adv-x="1024" />
+      
+      <glyph glyph-name="icon_unshare_b" unicode="&#59184;" d="M512 800a416 416 0 1 0 0-832 416 416 0 0 0 0 832zM310.208 656.64a339.2 339.2 0 0 1 337.024-583.744L310.144 656.768zM512 723.2c-48.128 0-93.888-10.048-135.296-28.096l336.96-583.68A339.2 339.2 0 0 1 512 723.2z"  horiz-adv-x="1024" />
+      
+      <glyph glyph-name="icon_organization_b" unicode="&#59185;" d="M704.512 832a64 64 0 0 0 64-64v-177.6l-0.32-6.528a64 64 0 0 0-57.152-57.152l-6.528-0.32H550.656v-87.488h288.512a70.4 70.4 0 0 0 70.4-70.4v-126.912h50.944l6.464-0.32a64 64 0 0 0 57.536-63.68V0l-0.384-6.528a64 64 0 0 0-57.152-57.088l-6.464-0.384h-177.92l-6.592 0.384a64 64 0 0 0-57.152 57.088L718.528 0v177.6a64 64 0 0 0 57.472 63.68l6.528 0.32h50.304V362.112h-282.24v-120.512h50.688l6.464-0.32a64 64 0 0 0 57.536-63.68V0l-0.384-6.528a64 64 0 0 0-57.152-57.088L601.28-64h-177.92l-6.592 0.384a64 64 0 0 0-57.152 57.088L359.296 0v177.6a64 64 0 0 0 57.472 63.68l6.528 0.32h50.56V362.112H181.824v-120.512h60.16l6.4-0.32a64 64 0 0 0 57.6-63.68V0l-0.448-6.528a64 64 0 0 0-57.088-57.088L241.92-64H64l-6.592 0.384a64 64 0 0 0-57.088 57.088L0 0v177.6a64 64 0 0 0 57.408 63.68L64 241.664h40.96V368.64a70.4 70.4 0 0 0 70.464 70.4h298.432V526.336H320.512l-6.592 0.32a64 64 0 0 0-57.088 57.152l-0.32 6.528V768a64 64 0 0 0 64 64h384zM76.8 12.8h152.32v152H76.8V12.8z m359.296 0h152.32v152h-152.32V12.8z m359.296 0h152.32v152h-152.32V12.8z m-462.08 590.4h358.4V755.2h-358.4v-152z"  horiz-adv-x="1024" />
+      
+      <glyph glyph-name="icon_sortingby_b" unicode="&#59186;" d="M786.368 152.832L889.6 256l38.464-38.528 38.528-38.4-196.032-196.032a54.464 54.464 0 0 0-72.96-3.584l-3.968 3.584-196.032 196.032L574.464 256l103.168-103.168V746.496h108.8v-593.664zM291.968 800.896c14.464 0 28.352-5.76 38.528-16l196.032-195.968-38.528-38.4-38.4-38.528-103.232 103.232v-593.728h-108.8V615.232L134.528 512 57.536 588.928l196.032 196.032 3.904 3.584a54.4 54.4 0 0 0 34.56 12.352z"  horiz-adv-x="1024" />
+      
+      <glyph glyph-name="icon_sales__order_b" unicode="&#59187;" d="M817.664 753.6a64 64 0 0 0 63.104-63.04l5.12-353.408c0-0.256 0.128-0.512 0.32-0.64a1.024 1.024 0 0 0 0-1.408L522.176-28.8a64 64 0 0 0-90.496 0L99.136 303.744a64 64 0 0 0 0 90.496l364.032 364.16v0.256c0 0.064 0 0.192 0.128 0.192l354.368-5.248z m-110.016-174.208A128 128 0 1 1 526.72 398.336a128 128 0 0 1 181.056 181.056z"  horiz-adv-x="1024" />
+      
+      <glyph glyph-name="icon_sendtest_b" unicode="&#59188;" d="M928 833.6a38.4 38.4 0 0 0 38.4-38.4v-822.4a38.4 38.4 0 0 0-38.4-38.4h-832a38.4 38.4 0 0 0-38.4 38.4V795.136a38.4 38.4 0 0 0 38.4 38.4h832zM134.4 11.2h755.2V756.864H134.4v-745.664z m328.128 531.456a38.4 38.4 0 0 0 30.272-23.04l78.464-184.448 63.04 77.056a38.464 38.464 0 0 0 29.76 14.08h154.88v-76.8H682.24l-91.648-112.128a38.4 38.4 0 0 0-65.088 9.344L447.552 429.632l-63.104-80.64a38.4 38.4 0 0 0-30.272-14.784h-147.2v76.8h128.64l91.648 117.248 3.392 3.712a38.4 38.4 0 0 0 31.872 10.624z"  horiz-adv-x="1024" />
+      
+      <glyph glyph-name="icon_time_full_b" unicode="&#59189;" d="M512 800a416 416 0 1 0 0-832 416 416 0 0 0 0 832z m-62.08-176.576V355.84a38.464 38.464 0 0 1 38.336-38.464h187.776v76.8H526.72V623.424h-76.8z"  horiz-adv-x="1024" />
+      
+      <glyph glyph-name="icon_datasource_b" unicode="&#59190;" d="M96 800a64 64 0 0 1-64-64v-704l0.32-6.528a64 64 0 0 1 57.088-57.152l6.592-0.32h832l6.528 0.32a64 64 0 0 1 57.472 63.68v704a64 64 0 0 1-64 64h-832z m819.2-755.2H108.8v173.76h806.4V44.8z m-730.56 47.872h128v76.8h-128v-76.8zM915.2 295.296H108.8V470.4h806.4v-175.04zM184.64 344.448h128v76.8h-128v-76.8zM915.2 547.072H108.8V723.2h806.4v-176.128zM184.64 596.16h128v76.8h-128v-76.8z"  horiz-adv-x="1024" />
+      
+      <glyph glyph-name="icon_unpack_b" unicode="&#59180;" d="M668.16 844.032a38.4 38.4 0 0 0 31.232 2.112l4.224-1.92 333.76-172.16a38.4 38.4 0 0 0 0.512-67.968l-85.312-45.76 85.12-44.928a38.4 38.4 0 0 0 0.512-67.648l-129.92-70.976v-241.6a38.4 38.4 0 0 0-20.352-33.92l-328.448-175.296a38.4 38.4 0 0 0-35.84-0.128l-334.976 175.36a38.4 38.4 0 0 0-20.544 33.92V376.576L41.28 445.76a38.4 38.4 0 0 0 0.512 67.648l85.12 44.928L41.6 604.16a38.4 38.4 0 0 0 0.512 67.968L375.872 844.16l4.224 1.92a38.4 38.4 0 0 0 31.296-2.112L539.776 776.32 668.096 844.032zM244.8 156.416l260.736-136.32V318.72l-98.56-52.928a38.4 38.4 0 0 0-36.544 0.128L244.864 334.528v-178.112z m464.128 109.504a38.4 38.4 0 0 0-36.544-0.128L582.4 314.24v-290.816l249.024 132.864v176.64l-122.432-66.944zM140.992 478.912L389.12 343.36l69.312 37.184-249.92 134.08-67.52-35.712z m480-98.368l69.376-37.12 248.128 135.488-67.584 35.712-249.92-134.08z m-330.624 177.28l249.408-133.632 249.28 133.696-249.28 131.648-249.408-131.648zM142.208 637.376l66.56-35.712 248.768 131.328-64.256 33.92-251.072-129.536z m479.68 95.616L870.656 601.6l66.688 35.712-251.072 129.536-64.384-33.92z"  horiz-adv-x="1088" />
+      
+      <glyph glyph-name="icon_vgm_n_b" unicode="&#59179;" d="M615.04 753.6a32 32 0 0 0 32-32v-64c0-1.088-0.32-2.176-0.448-3.264h67.264l8.32-0.384a96 96 0 0 0 85.312-74.496l101.12-448a96 96 0 0 0-82.368-116.48l-11.328-0.64H223.04a96 96 0 0 0-95.488 105.92l1.92 11.2 100.992 448a96 96 0 0 0 93.632 74.88h67.2c-0.128 1.088-0.32 2.176-0.32 3.264v64a32 32 0 0 0 32 32h192zM324.032 577.472a19.2 19.2 0 0 1-18.752-14.912l-101.056-448a19.2 19.2 0 0 1 18.752-23.424h591.872a19.2 19.2 0 0 1 18.752 23.424l-101.056 448a19.2 19.2 0 0 1-18.752 14.912h-389.76z m320.32-211.2c23.04 0 40.32-2.112 51.776-6.272 11.52-4.16 21.056-10.688 28.544-19.584 7.616-8.704 13.312-19.84 17.152-33.344l-62.4-11.136a33.664 33.664 0 0 1-13.12 18.112 40.448 40.448 0 0 1-23.36 6.272 42.432 42.432 0 0 1-33.92-14.848c-8.384-9.792-12.608-25.28-12.608-46.592 0-22.592 4.224-38.784 12.672-48.512 8.576-9.664 20.48-14.528 35.712-14.528 7.232 0 14.08 1.024 20.672 3.072 6.592 2.112 14.08 5.696 22.528 10.688v19.712h-43.2v43.52h99.84v-89.152a206.656 206.656 0 0 0-50.752-26.688c-14.72-4.672-32.064-7.04-52.224-7.04-24.832 0-45.12 4.288-60.8 12.736a87.488 87.488 0 0 0-36.352 37.76 124.8 124.8 0 0 0-12.8 57.664c0 22.848 4.672 42.688 14.08 59.52 9.408 16.896 23.232 29.76 41.408 38.464 14.144 6.784 33.216 10.176 57.152 10.176z m-283.136-3.584v-78.976l67.776 78.976h85.888l-76.16-78.848 79.552-130.304H438.72l-44.032 86.08-33.408-35.008v-51.072h-64.64V362.688h64.64z"  horiz-adv-x="1088" />
+      
+      <glyph glyph-name="icon_booking_resend" unicode="&#59178;" d="M114.858667 750.506667a21.333333 21.333333 0 0 0 30.634666 21.077333l751.786667-368.384a21.333333 21.333333 0 0 0 0-38.314667L168.192 7.594667l-22.741333-11.136a21.333333 21.333333 0 0 0-30.634667 21.034666l0.554667 3.242667 6.186666 24.618667 83.072 333.482666a21.461333 21.461333 0 0 1 0 10.325334L115.413333 747.221333l-0.554666 3.285334z m153.429333-398.677334l-62.208-249.6 509.44 249.6H268.288z m-0.938667 68.266667h440.32L206.08 665.813333l61.269333-245.76z"  horiz-adv-x="1024" />
+      
+      <glyph glyph-name="icon_hbl_reopen" unicode="&#59176;" d="M631.381333-41.984l-64-1.28-3.669333 179.413333 64 1.28 3.669333-179.413333z m237.482667 92.714667l-48.256-42.026667-117.802667 135.424 48.213334 41.984 117.845333-135.381333zM372.48 444.458667l-130.688-133.333334a113.066667 113.066667 0 0 1 161.536-158.250666l130.56 133.290666 26.026667-25.386666 25.856-25.344-130.645334-133.290667a185.6 185.6 0 0 0-265.088 259.712l130.56 133.290667 51.882667-50.688z m556.714667-114.005334l-0.298667-64-179.498667 0.810667 0.298667 64 179.498667-0.810667zM574.293333 751.018667a185.6 185.6 0 0 0 262.442667-262.485334l-131.925333-131.968-51.285334 51.285334 131.968 131.968A113.066667 113.066667 0 0 1 625.578667 699.733333L493.653333 567.765333l-25.642666 25.6-25.6 25.685334 131.925333 131.968zM244.309333 546.56l-1.621333-64-179.413333 4.48 1.578666 64 179.456-4.48z m49.066667 122.154667l-49.109333-41.002667-115.029334 137.813333 49.066667 40.96 115.072-137.813333z m139.178667 5.12h-64V853.333333h64v-179.498666z"  horiz-adv-x="1066" />
+      
+      <glyph glyph-name="icon_eci_st_retrigger" unicode="&#59177;" d="M751.658667 400h89.6a335.274667 335.274667 0 0 1-564.693334 225.706667L226.816 678.4a406.528 406.528 0 0 0 279.68 111.018667c219.050667 0 397.696-172.714667 407.296-389.418667h90.666667l-126.421334-125.098667-126.378666 125.098667z m-234.282667-421.461333c-219.050667 0-397.696 172.714667-407.296 389.418666H19.541333l126.421334 125.098667 126.378666-125.098667H182.613333a335.274667 335.274667 0 0 1 564.693334-225.664l49.749333-52.736a406.528 406.528 0 0 0-279.68-111.018666z"  horiz-adv-x="1024" />
+      
+      <glyph glyph-name="icon_next_b1" unicode="&#59175;" d="M706.112 336l-144.32-144.32a48 48 0 0 1 67.84-67.84l226.304 226.24a48 48 0 0 1 0 67.84L629.632 644.288a48 48 0 0 1-67.84-67.84l144.32-144.384h-476.16a48 48 0 1 1 0-96h476.16z"  horiz-adv-x="1088" />
+      
+      <glyph glyph-name="icon_default_screen_b" unicode="&#59174;" d="M372.864 283.84a38.4 38.4 0 0 0 38.4-38.4v-246.912h-76.8v208.512H126.08v76.8h246.848z m525.12-76.8h-208.512v-208.512H612.736v246.912a38.4 38.4 0 0 0 38.4 38.4h246.848v-76.8z m-486.656 315.52a38.4 38.4 0 0 0-38.4-38.4H126.08v76.8h208.512V769.408h76.736v-246.848z m278.144 38.4h208.512v-76.8h-246.848a38.4 38.4 0 0 0-38.4 38.4V769.408h76.8v-208.448z"  horiz-adv-x="1024" />
+      
+      <glyph glyph-name="icon_update__detail_b" unicode="&#59173;" d="M272.32 367.936H184.896a333.12 333.12 0 0 1 560.96-224.064l26.304-27.904 26.368-27.968a409.792 409.792 0 0 0-690.56 279.936H19.648L145.92 493.056 272.32 368z m234.24 423.68a409.792 409.792 0 0 0 409.408-391.552h88.448L878.08 274.944 751.68 400h87.424a333.12 333.12 0 0 1-560.96 224l-26.304 27.968-26.432 27.968a408.64 408.64 0 0 0 281.152 111.552zM371.264 422.4A38.4 38.4 0 1 0 371.2 345.6a38.4 38.4 0 0 0 0 76.8z m140.8 0A38.4 38.4 0 1 0 512 345.6a38.4 38.4 0 0 0 0 76.8z m140.8 0a38.4 38.4 0 1 0 0-76.8 38.4 38.4 0 0 0 0 76.8z"  horiz-adv-x="1024" />
+      
+      <glyph glyph-name="icon_brno_b" unicode="&#59165;" d="M839.04 832a64 64 0 0 0 64-64v-768l-0.32-6.528a64 64 0 0 0-57.152-57.152l-6.592-0.32h-640l-6.528 0.32a64 64 0 0 0-57.152 57.152L135.04 0V768a64 64 0 0 0 64 64h640z m-627.2-819.2h614.4V755.2h-614.4v-742.4z m531.2 80h-448v76.8h448v-76.8z m0 150.336h-448V319.872h448v-76.8zM532.096 658.816c4.096-0.448 8.064-1.024 12.032-1.792 2.048-0.448 4.032-1.024 6.08-1.536 1.984-0.512 4.032-0.896 5.952-1.472l5.824-2.048 4.8-1.728 2.176-0.896c1.152-0.448 2.304-1.088 3.456-1.6 2.112-0.96 4.224-1.92 6.272-3.008a128.32 128.32 0 0 0 53.952-54.08l2.816-5.76 2.432-5.376 2.176-6.08 1.92-5.376 1.6-6.336c0.448-1.856 0.96-3.712 1.344-5.632 0.448-2.112 0.64-4.352 1.024-6.528 0.832-6.08 1.472-12.288 1.472-18.56a128.64 128.64 0 0 0-5.632-37.184c-0.64-2.048-1.28-4.096-2.048-6.08l-1.92-5.376c-0.64-1.6-1.472-3.2-2.176-4.8a128.768 128.768 0 0 0-2.88-5.952c-0.896-1.728-1.92-3.392-2.88-5.056a128.128 128.128 0 0 0-10.496-15.424c-1.088-1.408-2.24-2.688-3.392-4.032l-4.8-5.312-3.392-3.264a129.152 129.152 0 0 0-36.16-24.512l-3.968-1.792-5.76-2.112c-2.176-0.768-4.288-1.536-6.464-2.176-1.728-0.512-3.456-0.896-5.12-1.28a128.832 128.832 0 0 0-31.36-4.032 129.984 129.984 0 0 0-25.28 2.56 127.744 127.744 0 0 0-11.392 2.816c-1.792 0.448-3.52 1.152-5.312 1.728a128.064 128.064 0 0 0-16.64 7.04c-1.536 0.832-3.136 1.6-4.672 2.496-1.92 1.088-3.84 2.304-5.696 3.456l-4.352 2.88a128.64 128.64 0 0 0-5.12 3.84c-1.28 1.024-2.624 1.92-3.84 3.008a129.216 129.216 0 0 0-16.384 16.32l-2.816 3.584a128.896 128.896 0 0 0-4.096 5.568l-2.944 4.48a128.512 128.512 0 0 0-8.448 15.68l-2.24 5.12a127.68 127.68 0 0 0-4.032 11.776l-1.408 5.312A128.512 128.512 0 0 0 519.04 659.456l13.12-0.64z"  horiz-adv-x="1088" />
+      
+      <glyph glyph-name="icon_smart_b" unicode="&#59166;" d="M768.512 315.328a32 32 0 0 0 55.488 0l32.704-56.704c2.432-4.224 5.76-7.872 9.856-10.56l49.664-33.408a32 32 0 0 0 0-53.12L866.56 128a31.936 31.936 0 0 1-9.856-10.56l-32.704-56.704a32 32 0 0 0-55.488 0l-32.064 55.68a32 32 0 0 1-11.776 11.712l-55.68 32.192a32 32 0 0 0 0 55.424l55.68 32.128a32 32 0 0 1 11.776 11.712l32.128 55.68zM408.192 707.2a32 32 0 0 0 55.424 0l100.48-174.08a32 32 0 0 1 9.728-10.56l150.4-101.12a32 32 0 0 0 0-53.12l-150.4-101.12a32 32 0 0 1-9.792-10.56l-100.48-174.08a32 32 0 0 0-55.36 0l-99.84 173.056a32 32 0 0 1-11.712 11.712l-173.056 99.84a32 32 0 0 0 0 55.488l173.056 99.84a32 32 0 0 1 11.712 11.648l99.84 173.056z"  horiz-adv-x="1088" />
+      
+      <glyph glyph-name="icon_map_b" unicode="&#59167;" d="M532.032 840.96a254.848 254.848 0 0 0 241.664-254.464l-0.64-16.768a290.688 290.688 0 0 0-15.36-69.248l199.36 53.312a38.4 38.4 0 0 0 48.32-37.056v-461.056a38.4 38.4 0 0 0-27.52-36.8l-307.904-90.624a38.528 38.528 0 0 0-24.96 1.152l-310.592 122.56-247.68-111.104a38.4 38.4 0 0 0-54.08 35.008V436.928a38.4 38.4 0 0 0 22.656 35.072l209.92 94.08-0.384 3.328-0.704 17.088A254.848 254.848 0 0 0 518.912 841.344l13.056-0.32zM109.312 412.096v-376.832l208.384 93.44 7.296 2.432a38.528 38.528 0 0 0 22.528-1.728l313.28-123.648 267.776 78.72V466.752l-212.8-56.96c-72.384-126.976-188.16-238.336-196.928-238.592l-1.856 0.64c-21.12 11.52-178.56 166.08-233.792 318.144L109.376 412.032z m409.6 352.448a178.048 178.048 0 0 1-178.048-177.92c0-29.248 10.112-65.92 30.464-107.904 19.968-41.344 47.424-83.072 76.48-121.024 24.768-32.384 49.92-60.8 70.72-82.624 20.864 22.144 46.272 51.2 71.232 83.968 27.136 35.712 52.864 74.624 72.448 113.152l-0.704 2.56 2.304 0.64 2.624 5.12c20.416 42.112 30.4 78.144 30.4 106.048a177.984 177.984 0 0 1-177.92 177.984z m10.368-65.92c51.84-5.248 92.352-49.088 92.352-102.4l-0.512-10.496A102.848 102.848 0 0 0 518.848 493.44l-10.56 0.512A102.976 102.976 0 0 0 416.512 585.6L416 596.16c0 56.832 46.08 102.912 102.848 102.912l10.496-0.512z m-10.496-76.288a26.112 26.112 0 1 1 0.064-52.224 26.112 26.112 0 0 1 0 52.224z"  horiz-adv-x="1088" />
+      
+      <glyph glyph-name="icon_template_b1" unicode="&#59168;" d="M898.56 559.872v-13.44c0.128-1.088 0.064-2.176 0-3.328V32a134.4 134.4 0 0 0-134.336-134.4H273.728a134.4 134.4 0 0 0-134.4 134.4v704c0 74.24 60.16 134.4 134.4 134.4h314.368l310.528-310.528zM273.792 793.6a57.6 57.6 0 0 1-57.6-57.6v-704l0.32-5.888a57.6 57.6 0 0 1 57.28-51.712h490.496a57.6 57.6 0 0 1 57.344 51.712l0.32 5.888V505.6h-153.6a134.4 134.4 0 0 0-134.4 134.336V793.6H273.664zM610.624 640c0-29.824 22.72-54.4 51.712-57.28l5.888-0.32h99.456L610.56 739.392v-99.456z"  horiz-adv-x="1088" />
+      
+      <glyph glyph-name="icon_door_b" unicode="&#59169;" d="M446.912 784a96 96 0 0 0 131.328 0.96l394.624-364.288-43.392-46.976-42.24 39.04v-423.104a32 32 0 0 0-32-32H618.432a32 32 0 0 0-32 32v174.08H441.152v-174.08a32 32 0 0 0-32-32H185.344a32 32 0 0 0-32 32V416l-44.096-41.984-44.16 46.336L446.976 784z m87.872-46.016a32 32 0 0 1-43.712-0.384L217.344 476.928v-455.296h159.808v174.08a32 32 0 0 0 32 32h209.344a32 32 0 0 0 32-32v-174.08h172.672V471.872L534.784 737.92z m317.184 17.92a32 32 0 0 0 32-32v-144.64h-64v112.64h-129.024v64h161.024z"  horiz-adv-x="1088" />
+      
+      <glyph glyph-name="icon_map_point_b" unicode="&#59170;" d="M823.296 462.677333c0-220.202667-301.653333-516.906667-317.013333-516.906666-15.445333 0-317.098667 291.584-317.098667 516.906666a317.056 317.056 0 1 0 634.112 0zM506.24 474.709333m-128 0a128 128 0 1 1 256 0 128 128 0 1 1-256 0Z"  horiz-adv-x="1066" />
+      
+      <glyph glyph-name="icon_current__location_b" unicode="&#59171;" d="M852.608 796.288c55.04 13.44 104.96-43.2 80.128-97.792L620.16 11.008c-31.36-68.864-134.464-46.528-134.464 29.184v325.76H169.216c-76.992 0-98.112 105.792-27.072 135.36l699.392 291.392 11.072 3.584zM201.216 442.752h361.152v-373.12L856.064 715.52 201.216 442.752z"  horiz-adv-x="1088" />
+      
+      <glyph glyph-name="icon_address_book_b" unicode="&#59172;" d="M887.04 832a64 64 0 0 0 64-64v-768l-0.32-6.528a64 64 0 0 0-63.744-57.472H215.04l-6.528 0.32a64 64 0 0 0-57.152 57.152L151.04 0v165.504h-64v76.8h64V525.696h-64v76.8h64V768a64 64 0 0 0 57.472 63.68L214.976 832h672zM227.84 602.496h64v-76.8h-64v-283.456h64v-76.8h-64V12.8h646.4V755.2H227.84v-152.704z m527.936-466.688h-398.72v76.8h398.72v-76.8z m0 214.4h-398.72V426.88h398.72v-76.8z m0 214.4h-398.72V641.28h398.72v-76.8z"  horiz-adv-x="1088" />
+      
+      <glyph glyph-name="icon_guideright_b" unicode="&#59163;" d="M870.4 384.512a38.4 38.4 0 0 1-35.392 38.016 237.824 237.824 0 0 0-211.84 177.28l-14.144 54.336-74.368-19.392 14.144-54.4A313.28 313.28 0 0 1 647.04 422.4H192v-76.8h456.896a319.936 319.936 0 0 1-100.8-160.896l-13.376-50.88 74.24-19.648 13.44 51.008a244.096 244.096 0 0 0 213.248 180.8 38.4 38.4 0 0 1 34.752 38.528z"  horiz-adv-x="1024" />
+      
+      <glyph glyph-name="icon_guideleft_b" unicode="&#59164;" d="M181.44 384.512a38.4 38.4 0 0 0 35.392 38.016A237.824 237.824 0 0 1 428.8 599.808l14.08 54.336 74.368-19.392-14.08-54.4A313.28 313.28 0 0 0 404.736 422.4h455.04v-76.8H402.944a319.936 319.936 0 0 0 100.736-160.896l13.376-50.88-74.24-19.648-13.44 51.008A244.096 244.096 0 0 1 216.32 345.984a38.4 38.4 0 0 0-34.816 38.528z"  horiz-adv-x="1088" />
+      
       <glyph glyph-name="icon_guidelines_b" unicode="&#59161;" d="M603.84-2.88a38.4 38.4 0 0 0 0-75.264l-7.744-0.704h-160a38.4 38.4 0 1 0 0 76.736h160l7.68-0.768zM516.096 647.36a269.376 269.376 0 0 0 144.576-496.512c-0.896-2.432-1.92-6.144-3.008-11.2a324.672 324.672 0 0 1-5.12-43.712 72.128 72.128 0 0 0-71.04-68.224l-127.296-0.832a72.32 72.32 0 0 0-72.064 67.648 298.24 298.24 0 0 1-5.76 43.52 74.304 74.304 0 0 1-3.712 12.16 269.312 269.312 0 0 0 143.424 497.216z m-234.24-489.408l27.136-27.2-76.928-76.864-54.208 54.272 76.8 76.928 27.264-27.136z m572.608-49.792l-54.272-54.272-76.928 76.864 27.136 27.2 27.2 27.136 76.864-76.928zM516.096 570.624a192.64 192.64 0 0 1-99.008-357.76l4.8-3.2c10.496-8 16.512-18.56 19.968-25.92 4.288-9.216 7.296-19.264 9.408-28.48 3.968-17.152 5.952-36.096 7.104-51.52l117.76 0.768c1.024 15.296 2.816 33.92 6.272 50.688 1.92 9.024 4.48 18.944 8.448 27.968a62.848 62.848 0 0 0 24.256 29.632l10.24 6.72a192.64 192.64 0 0 1-109.312 351.104zM184.96 350.144H72.448V426.88H184.96v-76.8z m774.784 0H847.36V426.88h112.448v-76.8zM309.056 653.76l-27.072-27.2-27.2-27.136-76.864 76.928 54.208 54.272 76.928-76.864z m545.472 22.592L777.6 599.424l-27.2 27.136-27.136 27.2 76.928 76.864 54.272-54.272z m-300.608 22.912h-76.8V807.488h76.8v-108.224z"  horiz-adv-x="1088" />
       
       <glyph glyph-name="icon_cancelled_b" unicode="&#59162;" d="M906.56 191.936l-27.136-27.136-80.896-80.768 80.896-80.896 27.2-27.136-54.4-54.336-27.072 27.2-80.896 80.896-80.768-80.896-27.136-27.2-54.336 54.336 27.2 27.136 80.832 80.896-80.832 80.768L582.016 192l54.336 54.336 27.136-27.2 80.768-80.832 80.896 80.832 27.136 27.2 54.272-54.336zM788.288 846.4a102.4 102.4 0 0 0 102.4-102.4v-458.24h-76.8v458.24a25.6 25.6 0 0 1-25.6 25.6H233.792a25.6 25.6 0 0 1-25.6-25.6v-704c0-14.08 11.456-25.6 25.6-25.6h277.248v-76.8H233.792a102.4 102.4 0 0 0-102.4 102.4v704a102.4 102.4 0 0 0 102.4 102.4h554.496z m-91.136-550.592H335.488v76.8h361.664v-76.8z m0 205.632H335.488v76.8h361.664v-76.8z"  horiz-adv-x="1088" />

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

+ 18 - 1
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;
@@ -354,6 +356,14 @@
   --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;
 }
 
 :root.dark {
@@ -481,6 +491,9 @@
   .el-input {
     --el-border: #656f7d;
   }
+  .el-date-editor {
+    --el-input-border-color: #656f7d;
+  }
   .el-radio {
     --el-radio-input-border: #656f7d;
   }
@@ -581,5 +594,9 @@
   --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;
 }
   

+ 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

+ 10 - 2
src/utils/axios.ts

@@ -76,24 +76,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
 

+ 5 - 1
src/utils/table.ts

@@ -6,7 +6,7 @@ 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 }[] = []
@@ -47,6 +47,10 @@ export const autoWidth = (tableData: VxeGridProps, grid: VxeGridInstance) => {
       if (field === 'Mode') {
         width = 80
       }
+      // 如果有自定义宽度,则使用自定义宽度
+      if (customizeWidth && customizeWidth[field]) {
+        width = customizeWidth[field]
+      }
 
       columnsWidth.push({
         width,

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

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

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

@@ -397,7 +397,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 () => {

+ 14 - 1
src/views/Dashboard/src/DashboardView.vue

@@ -1545,7 +1545,10 @@ function handleImageClick(event) {
 }
 .filters_left {
   border-radius: var(--border-radius-6);
-  flex: 1 40%;
+  width: calc(50% - 4px);
+  flex: 0 0 calc(50% - 4px);
+  min-width: 0;
+  box-sizing: border-box;
 }
 .KPI_title {
   border-bottom: 1px solid var(--color-border);
@@ -1576,6 +1579,16 @@ function handleImageClick(event) {
 .echarts {
   padding: 0 22px;
   background-color: var(--color-mode);
+  :deep(> div) {
+    display: flex;
+    flex-wrap: wrap;
+    justify-content: space-between;
+    gap: 8px;
+    width: 100%;
+    > * {
+      box-sizing: border-box;
+    }
+  }
 }
 .kpi {
   width: 50%;

+ 0 - 1
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)
       }
     })
 }

+ 221 - 190
src/views/DestinationDelivery/src/components/ConfiguRations/src/components/CreateNewRule.vue

@@ -42,37 +42,36 @@ 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. 检查基本条件是否满足
   if (
@@ -82,14 +81,14 @@ const CreateRuleDisabled = computed(() => {
     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
     }
   }
 
@@ -97,55 +96,54 @@ const CreateRuleDisabled = computed(() => {
   if (recommendRadio.value !== 1) {
     // 3.1 确保至少选择了一个运输方式
     if (recommendCheckedList.value.length === 0) {
-      return true;
+      return true
     }
 
     // 3.2 验证航空规则(如果选择了 Air)
-    console.log(recommendCheckedList.value)
     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;
+      const isAirValid = recommendCheckedAirList.value.every(
+        (item) =>
+          item.ports.length > 0 &&
+          item.recommended_delivery_from !== '' &&
+          item.recommended_delivery_to !== ''
+      )
+
+      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;
+      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
@@ -169,10 +167,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
@@ -189,17 +188,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 []
@@ -208,39 +207,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) {
+  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'
+    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;' 
+    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]
+      mergeData = [...mergeData, ...seaList]
     }
   }
-  if(seaList.length != 0) {
+  if (seaList.length != 0) {
     airData = Object.keys(seaList?.[0])
     airData.forEach((item) => {
       Object.assign(airlistInfo, {
@@ -248,36 +267,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>
@@ -286,11 +306,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;
@@ -355,7 +384,9 @@ onMounted(() => {
         </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>
@@ -364,107 +395,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-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>
+          </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>
@@ -550,7 +581,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);
 }
@@ -580,4 +611,4 @@ onMounted(() => {
 :deep(.el-dialog__body) {
   font-weight: 400;
 }
-</style>
+</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>

+ 78 - 51
src/views/DestinationDelivery/src/components/CreateNewBooking/src/CreateNewbooking.vue

@@ -27,13 +27,15 @@ 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)
@@ -198,10 +200,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
 }
@@ -227,34 +225,34 @@ const querySearchCountry = (query: string) => {
   Countryloading.value = true
   setTimeout(() => {
     $api
-    .getAddressCountryCityData({
-      term: query,
-      term_type: 'country',
-      limit: CityCode.value != ''  ? CityCode.value : ''
-    })
-    .then((res: any) => {
-      if (res.code === 200) {
-        Countryoptions.value = res.data
-        Countryloading.value = false
-      }
-    })
+      .getAddressCountryCityData({
+        term: query,
+        term_type: 'country',
+        limit: CityCode.value != '' ? CityCode.value : ''
+      })
+      .then((res: any) => {
+        if (res.code === 200) {
+          Countryoptions.value = res.data
+          Countryloading.value = false
+        }
+      })
   }, 1000)
 }
 const querySearchCity = (query: string) => {
   cityloading.value = true
   setTimeout(() => {
     $api
-    .getAddressCountryCityData({
-      term: query,
-      term_type: 'city',
-      limit: CountryCode.value != ''  ? CountryCode.value : ''
-    })
-    .then((res: any) => {
-      if (res.code === 200) {
-        Cityoptions.value = res.data
-        cityloading.value = false
-      }
-    })
+      .getAddressCountryCityData({
+        term: query,
+        term_type: 'city',
+        limit: CountryCode.value != '' ? CountryCode.value : ''
+      })
+      .then((res: any) => {
+        if (res.code === 200) {
+          Cityoptions.value = res.data
+          cityloading.value = false
+        }
+      })
   }, 1000)
 }
 // 特殊日期样式
@@ -310,12 +308,12 @@ const AddNewAddressDelivery = () => {
 // 保存新地址
 const SaveNewAddress = () => {
   if (
-    CountryCode.value != '' &&
-    CityCode.value != '' &&
-    PostalCode.value != '' &&
-    ContactPerson.value != '' &&
-    ContactNumber.value != '' &&
-    AddressLine1.value != '' ||
+    (CountryCode.value != '' &&
+      CityCode.value != '' &&
+      PostalCode.value != '' &&
+      ContactPerson.value != '' &&
+      ContactNumber.value != '' &&
+      AddressLine1.value != '') ||
     AddressLine2.value != '' ||
     AddressLine3.value != '' ||
     AddressLine4.value != ''
@@ -464,6 +462,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] : '',
@@ -616,7 +615,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' })
   }
@@ -730,11 +728,10 @@ onMounted(() => {
         >
       </div>
       <div class="shipments_search" v-if="a == undefined">
-        <div class="flex">
+        <div class="left-filter-search">
           <el-input
             placeholder="Enter Booking/HBL/PO/Carrier Booking No. "
             v-model="CreateNewBOokingSearch"
-            style="width: 34%"
             class="log_input"
           >
             <template #prefix>
@@ -769,14 +766,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"
@@ -796,7 +786,26 @@ onMounted(() => {
             ></CalendarDate>
             <span v-if="showataLabel" class="border-label">ATA</span>
           </div>
+
           <div class="input-with-label" style="margin-right: 8px">
+            <!-- <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"
@@ -804,11 +813,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>
+        <div class="right-btn">
+          <el-button
+            style="width: 108px"
+            class="el-button--dark create-button"
+            @click="SearchShipment"
+            >Search</el-button
+          >
         </div>
       </div>
       <NewbookingTable
@@ -908,8 +925,8 @@ onMounted(() => {
             :showToday="false"
             style="width: 240px"
             :format="userStore.dateFormat"
-            placeholder="Please Select Date"
             valueFormat="YYYY.MM.DD"
+            placeholder="Please Select Date"
           >
             <template #renderExtraFooter>
               <div class="recommended">
@@ -937,9 +954,14 @@ onMounted(() => {
             placeholder="Please Select Time"
           ></el-time-select>
         </div>
-        <div style="margin-left: 12px;">
+        <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-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>
@@ -1431,7 +1453,14 @@ onMounted(() => {
   font-weight: 400;
 }
 .shipments_search {
-  margin: 0 0 8px 0;
+  display: flex;
+  margin-bottom: 16px;
+}
+.left-filter-search {
+  flex: 1;
+  display: grid;
+  grid-template-columns: repeat(3, 1fr);
+  grid-gap: 8px;
 }
 :deep(.log_input .el-input__wrapper) {
   box-shadow: 0 0 0 1px var(--color-select-border) inset;
@@ -1439,8 +1468,6 @@ onMounted(() => {
 }
 .input-with-label {
   position: relative;
-  display: inline-block;
-  width: 34%;
 }
 .border-label {
   position: absolute;

+ 187 - 45
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, isCurrent: 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,15 @@ const handleColumns = (columns: any) => {
       curColumn = {
         ...curColumn,
         formatter: ({ cellValue }: any) => {
-          const array = cellValue.split("-")
+          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 +77,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 +105,24 @@ const getTableData = (val: any) => {
 const searchTableData = (val: any) => {
   tableLoadingTable.value = true
   $api
-  .BookingTableSearch({
-    ...val
-  })
-  .then((res: any) => {
-    if (res.code === 200) {
-      tableLoadingTable.value = false
-      tableData.value.data = res.data.data
-    }
-  })
+    .BookingTableSearch({
+      ...val
+    })
+    .then((res: any) => {
+      if (res.code === 200) {
+        tableLoadingTable.value = false
+        tableData.value.data = res.data.data
+      }
+    })
+    .finally(() => {
+      nextTick(() => {
+        tableRef.value &&
+          autoWidth(tableData.value, tableRef.value, {
+            packing_list: 190,
+            commercial_invoice: 180
+          })
+      })
+    })
 }
 
 const emits = defineEmits(['selectChangeEvent'])
@@ -116,79 +134,173 @@ 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]
+}
+
+// 将具有相同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 = () => {
+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)
+  })
 }
 
 // 实现行点击样式
@@ -201,11 +313,18 @@ defineExpose({
   getTableData,
   searchTableData
 })
-
 </script>
 
 <template>
-  <div class="SettingTable">
+  <div class="new-booking-table">
+    <el-button
+      style="width: 170px; align-self: flex-end"
+      type="default"
+      @click="handleCustomizeColumns"
+    >
+      <span style="margin-right: 6px" class="font_family icon-icon_column_b"></span>
+      Customize Columns
+    </el-button>
     <vxe-grid
       ref="tableRef"
       :style="{ border: 'none' }"
@@ -214,22 +333,29 @@ defineExpose({
       @checkbox-change="selectChangeEvent"
       @checkbox-all="selectAllChangeEvent"
     >
+      <!-- download下载的插槽 -->
+      <template #download="{ row, column }">
+        <div class="download-btn" @click="handleDownload(row.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 +383,20 @@ 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;
+}
 </style>

+ 10 - 1
src/views/Layout/src/components/Header/HeaderView.vue

@@ -215,6 +215,11 @@ const handleCloseThemePopover = () => {
   isPopoverVisible.value = false
   document.removeEventListener('click', handleClickOutside)
 }
+
+const handleDemoVideo = () => {
+  const { href } = router.resolve({ name: 'Demo Video' })
+  window.open(href, '_blank')
+}
 </script>
 
 <template>
@@ -352,7 +357,11 @@ const handleCloseThemePopover = () => {
             <span class="font_family icon-icon_manual_b"></span>
             User Manual
           </div>
-          <div class="item" @click="handleLogout">
+          <div class="item" style="margin-left: 1px" @click="handleDemoVideo">
+            <span class="font_family icon-icon_video_b"></span>
+            Demo Video
+          </div>
+          <div class="item" style="margin-left: 2px" @click="handleLogout">
             <span class="font_family icon-icon_export_b"></span>
             Logout
           </div>

+ 29 - 1
src/views/Layout/src/components/Header/components/TrainingCard.vue

@@ -11,7 +11,18 @@ const notificationMsgStore = useNotificationMessage()
 const notificationList = ref([])
 
 const curIndex = ref(0)
+const displayedList = ref([])
+const isNewCard = (id) => {
+  if (!id) return false
+  return !displayedList.value.some((item) => item.info.id === id)
+}
 const curCard = computed(() => {
+  if (
+    notificationList.value[curIndex.value] &&
+    isNewCard(notificationList.value[curIndex.value].info.id)
+  ) {
+    displayedList.value.push(notificationList.value[curIndex.value])
+  }
   return notificationList.value[curIndex.value] || null
 })
 
@@ -35,6 +46,11 @@ const trainingCardAfterLogin = () => {
   // 如果消息展示完毕,清除定时器
   if (curIndex.value >= notificationList.value.length) {
     clearInterval(trainingIntervalId)
+    // 将已经展示的消息标记为已读
+    notificationMsgStore.markMessageAsReadAfterCarousel(
+      displayedList.value.map((item) => item.info.id)
+    )
+    displayedList.value = []
   }
 }
 
@@ -54,6 +70,12 @@ const nextNotification = () => {
   if (curIndex.value >= notificationList.value.length) {
     clearInterval(trainingIntervalId)
     result = false
+
+    // 将已经展示的消息标记为已读
+    notificationMsgStore.markMessageAsReadAfterCarousel(
+      displayedList.value.map((item) => item.info.id)
+    )
+    displayedList.value = []
   }
   return result
 }
@@ -104,6 +126,12 @@ const getNotificationList = (time?: string) => {
 
 // 关闭消息,如果是登录后自动轮播的消息,则需清除定时器,如果不是则继续轮播下一条消息
 const closeMessage = () => {
+  // 将已经展示的消息标记为已读
+  notificationMsgStore.markMessageAsReadAfterCarousel(
+    displayedList.value.map((item) => item.info.id)
+  )
+  displayedList.value = []
+
   if (!newMessageIntervalId) {
     clearInterval(trainingIntervalId)
     curIndex.value = -1
@@ -132,9 +160,9 @@ const closeMessage = () => {
       <span class="font_family icon-icon_reject_b"></span>
     </el-button>
     <NotificationMessageCard
-      :isObserver="false"
       v-if="curCard"
       :data="[curCard]"
+      :isObserver="false"
       :topOffset="0"
       :isDrawer="true"
     ></NotificationMessageCard>

+ 24 - 14
src/views/Layout/src/components/Menu/MenuView.vue

@@ -11,12 +11,12 @@ const userStore = useUserStore()
 const isCollapse = defineModel<boolean>()
 
 const menuList = ref()
-watch(
-  () => userStore.userInfo?.uname,
-  () => {
-    getMenuList()
-  }
-)
+// watch(
+//   () => userStore.userInfo?.uname,
+//   () => {
+//     getMenuList()
+//   }
+// )
 const getMenuList = () => {
   $api.getMenuList().then((res) => {
     if (res.code === 200) {
@@ -34,7 +34,6 @@ const getMenuList = () => {
   //     index: '2',
   //     label: 'Booking',
   //     icon: 'icon_booking__fill_b',
-  //     // path: '/booking',
   //     type: 'list',
   //     children: [
   //       {
@@ -57,37 +56,48 @@ const getMenuList = () => {
   //   },
   //   {
   //     index: '4',
+  //     label: 'Report',
+  //     icon: 'icon_report__fill_b',
+  //     path: '/report'
+  //   },
+  //   {
+  //     index: '5',
   //     label: 'System Management',
   //     icon: 'icon_system__management_fill_b',
   //     type: 'list',
   //     children: [
   //       {
-  //         index: '4-1',
+  //         index: '5-7',
+  //         label: 'Template Management',
+  //         path: '/template-management'
+  //       },
+  //       {
+  //         index: '5-1',
   //         label: 'System Message',
   //         path: '/system-message'
   //       },
   //       {
-  //         index: '4-2',
+  //         index: '5-2',
   //         label: 'System Settings',
   //         path: '/SystemSettings'
   //       },
   //       {
-  //         index: '4-3',
+  //         index: '5-3',
   //         label: 'Chat Log',
   //         path: '/chat-log'
   //       },
   //       {
-  //         index: '4-4',
+  //         index: '5-4',
   //         label: 'AI API Log',
   //         path: '/ai-api-log'
   //       },
   //       {
-  //         index: '4-5',
+  //         index: '5-5',
   //         label: 'Operation Log',
   //         path: '/Operationlog'
   //       },
   //       {
-  //         index: '4-6',
+  //         index: '5-6',
   //         label: 'Prompt Configuration',
   //         path: '/PromptConfiguration'
   //       }
@@ -152,7 +162,7 @@ const changeRouter = (path: any) => {
     localStorage.removeItem('loginAI')
     emitter.emit('login-success')
   }
-  if (path == '/PromptConfiguration') {
+  if (path == '/PromptConfiguration' || path == '/report') {
     emitter.emit('checkPrompt')
   } else {
     if (localStorage.getItem('loginAI')) {

+ 1 - 0
src/views/Report/index.ts

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

+ 238 - 0
src/views/Report/src/ReportView.vue

@@ -0,0 +1,238 @@
+<script setup lang="ts">
+import { ref, onMounted } from 'vue'
+import { type VxeGridInstance, type VxeGridProps } from 'vxe-table'
+import { useRowClickStyle } from '@/hooks/rowClickStyle'
+import { formatNumber } from '@/utils/tools'
+import { useRouter } from 'vue-router'
+import { useCalculatingHeight } from '@/hooks/calculatingHeight'
+import emptyImgLight from './images/empty-light.png'
+import emptyImgDark from './images/empty-dark.png'
+import { useThemeStore } from '@/stores/modules/theme'
+
+const router = useRouter()
+const themeStore = useThemeStore()
+
+const textSearch = ref('')
+// search report name
+const SearchInput = () => {
+  getTableData()
+}
+
+const emptyImg = computed(() => {
+  return themeStore.theme === 'dark' ? emptyImgDark : emptyImgLight
+})
+
+const filterRef: Ref<HTMLElement | null> = ref(null)
+const containerHeight = useCalculatingHeight(document.documentElement, 256, [filterRef])
+
+const tableData = ref<VxeGridProps<any>>({
+  border: true,
+  round: true,
+  columns: [
+    {
+      title: 'Action',
+      fixed: 'left',
+      width: 178,
+      slots: { default: 'action' }
+    },
+    {
+      field: 'name',
+      title: 'Report Name'
+    },
+    {
+      field: 'description',
+      title: 'Description'
+    }
+  ],
+  data: [],
+  scrollY: { enabled: true, oSize: 20, gt: 30 },
+  stripe: true,
+  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)
+const tableLoading = ref(false)
+const pageInfo = ref({ pageNo: 1, pageSize: 50, total: 0 })
+
+// 获取表格数据
+const getTableData = (isPageChange?: boolean) => {
+  tableLoading.value = true
+  $api
+    .getReportPageTable({
+      cp: pageInfo.value.pageNo,
+      ps: pageInfo.value.pageSize,
+      rc: isPageChange ? pageInfo.value.total : -1,
+      text_search: textSearch.value
+    })
+    .then((res: any) => {
+      if (res.code === 200) {
+        pageInfo.value.total = Number(res.data.rc)
+        pageInfo.value.pageNo = res.data.cp
+        pageInfo.value.pageSize = res.data.ps
+        tableData.value.data = res.data.searchData || []
+      }
+    })
+    .finally(() => {
+      tableLoading.value = false
+    })
+}
+
+// 实现行点击样式
+useRowClickStyle(tableRef)
+
+const handleClickDetail = (row: any) => {
+  router.push({
+    name: 'ReportDetail',
+    query: {
+      id: row.serial_no
+    }
+  })
+}
+const handleClickSchedule = (row: any) => {
+  router.push({
+    name: 'ReportSchedule',
+    query: {
+      id: row.serial_no
+    }
+  })
+}
+
+onMounted(() => {
+  getTableData()
+})
+</script>
+
+<template>
+  <div class="Title">
+    <span>Report</span>
+  </div>
+  <div class="report_search">
+    <div class="search">
+      <el-input
+        placeholder="Search Report Name"
+        v-model="textSearch"
+        class="log_input"
+        @keyup.enter="SearchInput"
+      >
+        <template #prefix>
+          <span class="font_family icon-icon_search_b"></span>
+        </template>
+      </el-input>
+    </div>
+    <el-button class="el-button--dark" style="margin-left: 8px" @click="SearchInput"
+      >Search</el-button
+    >
+  </div>
+  <div class="SettingTable">
+    <vxe-grid
+      ref="tableRef"
+      :style="{ border: 'none' }"
+      v-bind="tableData"
+      :height="containerHeight"
+      v-vloading="tableLoading"
+    >
+      <!-- 空数据时的插槽 -->
+      <template #empty>
+        <img :src="emptyImg" />
+        <div class="empty-text">No data</div>
+      </template>
+      <template #action="{ row }">
+        <el-button class="el-button--blue" @click="handleClickDetail(row)" style="height: 24px">
+          <span class="font_family icon-icon_edoc_b"></span>
+          <span style="font-size: 12px">Details</span>
+        </el-button>
+        <el-button class="el-button--blue" style="height: 24px" @click="handleClickSchedule(row)">
+          <span class="font_family icon-icon_time_b"></span>
+          <span style="font-size: 12px">Schedule</span>
+        </el-button>
+      </template>
+    </vxe-grid>
+  </div>
+  <div class="pagination">
+    <span>Total {{ formatNumber(pageInfo.total) }}</span>
+    <el-pagination
+      v-model:current-page="pageInfo.pageNo"
+      v-model:page-size="pageInfo.pageSize"
+      :page-sizes="[50, 100, 200, 300, 400]"
+      :pagerCount="5"
+      background
+      layout="sizes, prev, pager, next"
+      :total="pageInfo.total"
+      @size-change="getTableData(true)"
+      @current-change="getTableData(true)"
+    />
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.Title {
+  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;
+}
+.report_search {
+  border: 1px solid var(--color-border);
+  border-width: 0 0 1px 0;
+  padding: 8px 24px;
+  display: flex;
+}
+.search {
+  width: 400px;
+  height: 32px;
+}
+.SettingTable {
+  margin: 8px 24px 0 24px;
+  font-weight: 400;
+}
+.icon-icon_delete_b::before {
+  color: var(--color-btn-danger-bg);
+}
+.icon_alert::before {
+  color: var(--color-btn-warning-bg);
+}
+.delete_title {
+  font-size: 18px;
+  font-weight: 700;
+  padding: 20px 16px;
+  color: var(--color-neutral-1);
+}
+.delete_content {
+  font-size: 14px;
+  font-weight: 400;
+  color: var(--color-neutral-1);
+  padding: 15px 0 33px 37px;
+}
+.pagination {
+  display: flex;
+  justify-content: space-between;
+  font-weight: 400;
+  font-size: 15px;
+  align-items: center;
+  margin: 0 24px;
+  border: 1px solid var(--color-border);
+  border-top: none;
+  padding: 4px 8px;
+  border-radius: 0 0 6px 6px;
+}
+:deep(.el-icon svg) {
+  width: 1em !important;
+}
+.empty-text {
+  margin: 8px 0;
+  color: var(--color-neutral-2);
+  text-align: center;
+  font-size: 14px;
+  font-weight: 700;
+}
+</style>

+ 1 - 0
src/views/Report/src/components/ReportDetail/index.ts

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

+ 265 - 0
src/views/Report/src/components/ReportDetail/src/ReportDetail.vue

@@ -0,0 +1,265 @@
+<script setup lang="ts">
+import ManageReportFields from './components/ManageReportFields.vue'
+import FieldsTable from './components/FieldsTable.vue'
+import { useCalculatingHeight } from '@/hooks/calculatingHeight'
+import { useUserStore } from '@/stores/modules/user'
+import { useRoute } from 'vue-router'
+
+const route = useRoute()
+
+const userStore = useUserStore()
+const formatDate = userStore.dateFormat
+const filterRef: Ref<HTMLElement | null> = ref(null)
+const containerHeight = useCalculatingHeight(document.documentElement, 336, [filterRef])
+
+const DownloadVisible = ref(false)
+const isFolded = ref(false)
+const ManageReportFieldsRef = ref()
+const fieldsTableRef = ref()
+
+const filterList = ref([])
+const handleFilterData = (data) => {
+  if (filterList.value.length) return
+  filterList.value = data.map((item) => {
+    let curData: any = {}
+    if (item.data_type === 'string') {
+      curData.value = ''
+    } else if (item.data_type === 'number' || item.data_type === 'date') {
+      curData.value = []
+    }
+    return {
+      label: item.label,
+      field: item.field,
+      type: item.data_type,
+      ...curData
+    }
+  })
+}
+
+const handleClickManageFields = () => {
+  ManageReportFieldsRef.value.openDialog(route.query.id as string)
+}
+
+const handelSearchFilters = () => {
+  fieldsTableRef.value.handleSearch()
+}
+const handleClickReset = () => {
+  filterList.value.forEach((item) => {
+    if (item.type === 'string') {
+      item.value = ''
+    } else if (item.type === 'number' || item.type === 'date') {
+      item.value = []
+    }
+  })
+}
+const downloadCoulumn = ref()
+const handleClickDownload = (val: string) => {
+  DownloadVisible.value = false
+  fieldsTableRef.value.getExportTableData(val, downloadCoulumn.value)
+}
+
+const applyNewColumn = () => {
+  fieldsTableRef.value.handleSearch()
+}
+</script>
+<template>
+  <div>
+    <div class="Title">
+      <span>Shipment Status Report</span>
+      <div>
+        <el-popover placement="bottom" width="195" :visible="DownloadVisible">
+          <p class="download-item" @click="handleClickDownload('xlsx')">Excel(.xlsx)</p>
+          <p class="download-item" @click="handleClickDownload('csv')">CSV(.csv)</p>
+          <template #reference>
+            <el-button
+              @blur="DownloadVisible = false"
+              @click="DownloadVisible = !DownloadVisible"
+              class="el-button--main download_button"
+            >
+              <span class="font_family icon-icon_download_b"></span
+              ><span style="margin: 0 4px">Download Report</span
+              ><span class="font_family icon-icon_dropdown_b"></span>
+            </el-button>
+          </template>
+        </el-popover>
+        <el-button type="default" @click="handleClickManageFields">
+          <span class="font_family icon-icon_set_b"></span>Manage Fields
+        </el-button>
+        <ManageReportFields
+          ref="ManageReportFieldsRef"
+          @applyNewColumn="applyNewColumn"
+        ></ManageReportFields>
+      </div>
+    </div>
+    <div class="filters" :class="{ 'fold-filter': isFolded }">
+      <div class="filers-flex">
+        <div class="filters-title">
+          <span
+            class="font_family icon-icon_dropdown_b"
+            :class="{ 'fold-class': isFolded }"
+            @click="isFolded = !isFolded"
+          ></span>
+          Filters
+        </div>
+        <div>
+          <el-button @click="handleClickReset" style="height: 32px; width: 80px" type="default"
+            ><span class="font_family icon-icon_reset_b" style="margin-right: 8px"></span
+            >Reset</el-button
+          >
+          <el-button
+            style="height: 32px; width: 80px"
+            class="el-button--dark"
+            @click="handelSearchFilters"
+            >Search</el-button
+          >
+        </div>
+      </div>
+      <div class="filter-search" ref="filterRef">
+        <div class="filters-input" v-for="item in filterList" :key="item.label">
+          <div class="filters-input-title">{{ item.label }}</div>
+          <el-input
+            v-if="item.type === 'string'"
+            placeholder="Please enter..."
+            v-model="item.value"
+          ></el-input>
+          <el-select
+            v-else-if="item.type === 'select'"
+            v-model="item.value"
+            placeholder="Please select..."
+          >
+            <el-option
+              v-for="ite in item.options"
+              :key="ite.value"
+              :label="ite.label"
+              :value="ite.value"
+            />
+          </el-select>
+          <div v-if="item.type === 'number'" style="display: flex; gap: 4px; align-items: center">
+            <el-input
+              placeholder="Please enter..."
+              class="no-spinner"
+              type="number"
+              v-model="item.value[0]"
+            ></el-input>
+            -
+            <el-input
+              placeholder="Please enter..."
+              class="no-spinner"
+              type="number"
+              v-model="item.value[1]"
+            ></el-input>
+          </div>
+          <div v-if="item.type === 'date'" style="display: flex; gap: 4px; align-items: center">
+            <el-date-picker
+              v-model="item.value"
+              type="daterange"
+              range-separator="To"
+              start-placeholder="Start date"
+              end-placeholder="End date"
+              style="height: 32px"
+              value-format="MM/DD/YYYY"
+              :format="formatDate"
+            />
+
+            <!-- <el-date-picker
+              v-model="item.value[0]"
+              type="date"
+              placeholder="Pick a Date"
+              :format="formatDate"
+              valueFormat="MM/DD/YYYY"
+            />
+            -
+            <el-date-picker
+              v-model="item.value[1]"
+              type="date"
+              placeholder="Pick a Date"
+              :format="formatDate"
+              valueFormat="MM/DD/YYYY"
+            /> -->
+          </div>
+        </div>
+      </div>
+    </div>
+    <FieldsTable
+      ref="fieldsTableRef"
+      :containerHeight="containerHeight"
+      :filterData="filterList"
+      @filterOptionsLoaded="handleFilterData"
+    ></FieldsTable>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.Title {
+  display: flex;
+  justify-content: space-between;
+  height: 68px;
+  border-bottom: 1px solid var(--color-border);
+  font-size: var(--font-size-6);
+  font-weight: 700;
+  padding: 0 24px;
+  align-items: center;
+}
+.download_button {
+  padding: 8px 21px;
+}
+.download-item {
+  margin: 8px;
+  height: 40px;
+  padding: 9px 8px;
+  border-radius: 6px;
+}
+.download-item:hover {
+  background-color: var(--color-table-row-hover-bg);
+}
+.download-item:active {
+  color: var(--color-theme);
+}
+.filters {
+  margin: 8px 24px;
+  padding: 8px 8px 8px 16px;
+  border: 1px solid var(--color-border);
+  border-radius: 12px;
+  .filers-flex {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    margin-bottom: 20px;
+  }
+  .filters-title {
+    font-size: 18px;
+    font-weight: 700;
+    .icon-icon_dropdown_b {
+      display: inline-block;
+      transition: transform 0.3s;
+      cursor: pointer;
+    }
+    .fold-class {
+      transform: rotate(-90deg) !important;
+    }
+  }
+  .filter-search {
+    display: grid;
+    grid-template-columns: repeat(auto-fill, minmax(236px, 1fr));
+    grid-auto-rows: auto;
+    gap: 8px;
+    .filters-input {
+      .filters-input-title {
+        font-size: 12px;
+        font-weight: 400;
+        color: var(--color-neutral-2);
+        margin-bottom: 4px;
+      }
+    }
+  }
+}
+:deep(.no-spinner input::-webkit-inner-spin-button),
+:deep(.no-spinner input::-webkit-outer-spin-button) {
+  -webkit-appearance: none;
+  margin: 0;
+}
+
+:deep(.no-spinner input[type='number']) {
+  -moz-appearance: textfield;
+}
+</style>

+ 314 - 0
src/views/Report/src/components/ReportDetail/src/components/FieldsTable.vue

@@ -0,0 +1,314 @@
+<script setup lang="ts">
+import { ref, onMounted } from 'vue'
+import { type VxeGridInstance, type VxeGridProps } from 'vxe-table'
+import { useRowClickStyle } from '@/hooks/rowClickStyle'
+import { formatNumber } from '@/utils/tools'
+import dayjs from 'dayjs'
+import { autoWidth } from '@/utils/table'
+import { useRoute } from 'vue-router'
+
+const route = useRoute()
+// const filterRef: Ref<HTMLElement | null> = ref(null)
+
+const props = defineProps<{
+  containerHeight: any
+  filterData: any[]
+}>()
+
+const sortBy = ref('')
+const sortOrder = ref('')
+const sortByOptions = ref([])
+const sortOptions = ref([
+  {
+    label: 'Ascending',
+    value: 'asc'
+  },
+  {
+    label: 'Descending',
+    value: 'desc'
+  }
+])
+
+const tableLoadingTable = ref(false)
+const exportLoading = ref(false)
+
+const tableData = ref<VxeGridProps<any>>({
+  border: true,
+  round: true,
+  columns: [],
+  data: [],
+  scrollY: { enabled: true, oSize: 20, gt: 30 },
+  stripe: true,
+  emptyText: ' ',
+  showHeaderOverflow: true,
+  showOverflow: true,
+  headerRowStyle: {
+    backgroundColor: 'var(--color-table-header-bg)'
+  },
+  columnConfig: { resizable: true, useKey: true },
+  rowConfig: { isHover: true },
+  exportConfig: {
+    types: ['csv', 'html', 'txt', 'xlsx'],
+    modes: ['current', 'selected', 'all']
+  }
+})
+
+const allTableRef = ref<VxeGridInstance>()
+const allTable = ref<VxeGridProps<any>>({
+  columns: [],
+  data: [],
+  showHeaderOverflow: true,
+  showOverflow: true,
+  scrollY: { enabled: true, oSize: 5, gt: 2 },
+  scrollX: { enabled: true, gt: 2 },
+  exportConfig: {
+    types: ['csv', 'html', 'txt', 'xlsx'],
+    modes: ['current', 'selected', 'all']
+  }
+})
+
+const tableRef = ref<VxeGridInstance | null>(null)
+const pageInfo = ref({ pageNo: 1, pageSize: 50, total: 0 })
+const reportName = ref('')
+
+const handleColumns = (columns: any) => {
+  const newColumns = columns.map((item: any) => {
+    let curColumn: any = {
+      title: item.title,
+      field: item.field,
+      width: item.width
+    }
+    // 设置插槽
+    if (item.type === 'status') {
+      curColumn = {
+        ...curColumn,
+        slots: { default: 'status' }
+      }
+    }
+    return curColumn
+  })
+  return newColumns
+}
+const emit = defineEmits(['filterOptionsLoaded'])
+
+// 获取表格数据
+const getTableData = (isPageChange?: boolean) => {
+  tableLoadingTable.value = true
+  let queryParams = {}
+  props.filterData.forEach((item) => {
+    if (item.type === 'string') {
+      queryParams[item.field] = item.value
+    } else if (item.type === 'number' || item.type === 'date') {
+      queryParams[item.field] = item.value
+    }
+  })
+  $api
+    .getReportDetailTable({
+      cp: pageInfo.value.pageNo,
+      ps: pageInfo.value.pageSize,
+      rc: isPageChange ? pageInfo.value.total : -1,
+      serial_no: route.query.id,
+      ...queryParams
+    })
+    .then((res: any) => {
+      if (res.code === 200) {
+        const tableDataValue = res.data.tableData
+
+        pageInfo.value.total = Number(tableDataValue.rc)
+        pageInfo.value.pageNo = tableDataValue.cp
+        pageInfo.value.pageSize = tableDataValue.ps
+        tableData.value.data = tableDataValue.searchData
+        tableData.value.columns = handleColumns(res.data.tableColumns)
+        allTable.value.columns = handleColumns(res.data.tableColumns)
+
+        tmpSearch = res.data.tmp_search
+
+        reportName.value = res.data.reportName
+
+        const sortByData = res.data.sortBy
+        sortByOptions.value = sortByData.options
+        sortBy.value = sortByData.field
+        sortOrder.value = sortByData.order
+        emit('filterOptionsLoaded', res.data.filtersList)
+      }
+    })
+    .finally(() => {
+      nextTick(() => {
+        tableRef.value && autoWidth(tableData.value, tableRef.value)
+
+        tableLoadingTable.value = false
+      })
+    })
+}
+
+// 查询时调用接口
+const handleSearch = () => {
+  getTableData()
+}
+
+let tmpSearch = ''
+// 下载
+const getExportTableData = async (type: string, column: any) => {
+  if (column) {
+    const newColumns = column.map((item: any) => {
+      let curColumn: any = {
+        title: item.displayName != '' ? item.displayName : item.title,
+        field: item.field,
+        width: item.width
+      }
+      return curColumn
+    })
+    column = newColumns
+  }
+  exportLoading.value = true
+  await $api.getReportAllTableData({ tmp_search: tmpSearch }).then((res: any) => {
+    if (res.code === 200) {
+      allTable.value.data = res.data.Data || []
+      if (allTable.value.data.length > 20000) {
+        type = 'csv'
+      }
+    } else if (res.code === 500) {
+      ElMessage.error(res.data.msg)
+    }
+  })
+  await nextTick(async () => {
+    await autoWidth(allTable.value, allTableRef.value)
+  })
+  const exportConfig: any = {
+    type: type,
+    message: false,
+    filename: `${reportName.value || 'Report List'}_${dayjs().format('YYYYMMDDHH[h]mm[m]ss[s]')}`
+    // columns: column || tableData.value.columns
+  }
+  allTableRef.value.exportData(exportConfig)
+  exportLoading.value = false
+}
+
+// 实现行点击样式
+useRowClickStyle(tableRef)
+
+onMounted(() => {
+  getTableData()
+})
+
+defineExpose({
+  handleSearch,
+  getExportTableData
+})
+</script>
+<template>
+  <div
+    v-loading.fullscreen.lock="exportLoading"
+    element-loading-text="Loading..."
+    element-loading-custom-class="element-loading"
+    element-loading-background="rgb(43, 47, 54, 0.7)"
+  >
+    <div class="setting-table">
+      <div class="flex">
+        <div class="title">Report Data Review</div>
+        <div class="flex" style="margin-bottom: 6px">
+          <span class="sort-text">Sort by:</span>
+          <el-select
+            style="width: 200px; margin: 0 8px"
+            v-model="sortBy"
+            placeholder="Please select..."
+          >
+            <el-option v-for="item in sortByOptions" :key="item" :label="item" :value="item" />
+          </el-select>
+          <el-select style="width: 124px" v-model="sortOrder" placeholder="Please select...">
+            <el-option
+              v-for="item in sortOptions"
+              :key="item.value"
+              :label="item.label"
+              :value="item.value"
+            />
+          </el-select>
+        </div>
+      </div>
+      <vxe-grid
+        ref="tableRef"
+        :style="{ border: 'none' }"
+        v-bind="tableData"
+        :height="props.containerHeight"
+        v-vloading="tableLoadingTable"
+      >
+        <!-- 空数据时的插槽 -->
+        <template #empty>
+          <img src="../images/default_no_data@2x.png" />
+          <div class="empty-text">No data</div>
+        </template>
+        <!-- Status字段的插槽 -->
+        <template #status="{ row, column }">
+          <VTag :type="row[column.field]">{{ row[column.field] }}</VTag>
+        </template>
+      </vxe-grid>
+      <vxe-grid :height="10" ref="allTableRef" class="all-table" v-bind="allTable"> </vxe-grid>
+    </div>
+    <div class="pagination">
+      <span>Total {{ formatNumber(pageInfo.total) }}</span>
+      <el-pagination
+        v-model:current-page="pageInfo.pageNo"
+        v-model:page-size="pageInfo.pageSize"
+        :page-sizes="[50, 100, 200, 300, 400]"
+        :pagerCount="5"
+        background
+        layout="sizes, prev, pager, next"
+        :total="pageInfo.total"
+        @size-change="getTableData(true)"
+        @current-change="getTableData(true)"
+      />
+    </div>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.flex {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  .sort-text {
+    font-size: 12px;
+    font-weight: 400;
+    color: var(--color-neutral-2);
+  }
+}
+.title {
+  font-size: 18px;
+  font-weight: 700;
+  margin-bottom: 13px;
+}
+.pagination {
+  display: flex;
+  justify-content: space-between;
+  font-weight: 400;
+  font-size: 15px;
+  align-items: center;
+  margin: 0 24px;
+  border: 1px solid var(--color-border);
+  border-top: none;
+  padding: 4px 8px;
+  border-radius: 0 0 6px 6px;
+}
+:deep(.el-icon svg) {
+  width: 1em !important;
+}
+.setting-table {
+  position: relative;
+  overflow: hidden;
+  padding: 13px 24px 0 24px;
+  font-weight: 400;
+  border-top: 1px solid var(--color-border);
+  .all-table {
+    position: absolute;
+    top: -100000px;
+    width: 20px;
+  }
+}
+.empty-text {
+  margin: 8px 0;
+  color: var(--color-neutral-2);
+  text-align: center;
+  font-size: 14px;
+  font-weight: 700;
+}
+</style>

+ 261 - 0
src/views/Report/src/components/ReportDetail/src/components/ManageReportFields.vue

@@ -0,0 +1,261 @@
+<script setup lang="ts">
+import { VueDraggable } from 'vue-draggable-plus'
+interface ColumnItem {
+  id: string
+  field_display_name: string
+  is_enabled: boolean
+  field_display_name_user: string
+}
+
+const manageFieldsVisible = ref(false)
+const manageFieldsColumns = ref<ColumnItem[]>([])
+const loading = ref(false)
+const serialNo = ref('')
+const openDialog = (id: string) => {
+  loading.value = true
+  manageFieldsVisible.value = true
+  serialNo.value = id
+  $api
+    .getManageFieldsList({ serial_no: id })
+    .then((res: any) => {
+      if (res.code === 200) {
+        manageFieldsColumns.value = res.data.data
+      }
+    })
+    .finally(() => {
+      loading.value = false
+    })
+}
+const curSelectedNumber = computed(() => {
+  if (!manageFieldsColumns.value) return 0
+  return manageFieldsColumns.value.filter((item: any) => item.is_enabled).length
+})
+const handleMoveUpSelect = (data: any) => {
+  const index = manageFieldsColumns.value.findIndex((item: any) => item.id === data.id)
+  if (index === 0) return
+  const temp = manageFieldsColumns.value[index]
+  manageFieldsColumns.value[index] = manageFieldsColumns.value[index - 1]
+  manageFieldsColumns.value[index - 1] = temp
+}
+const handleMoveDownSelect = (data: any) => {
+  const index = manageFieldsColumns.value.findIndex((item: any) => item.id === data.id)
+  if (index === manageFieldsColumns.value.length - 1) return
+  const temp = manageFieldsColumns.value[index]
+  manageFieldsColumns.value[index] = manageFieldsColumns.value[index + 1]
+  manageFieldsColumns.value[index + 1] = temp
+}
+
+const ShowAll = (type: string) => {
+  manageFieldsColumns.value.forEach((item: any) => {
+    item.is_enabled = type === 'show'
+  })
+}
+const emit = defineEmits(['applyNewColumn'])
+const handleApply = () => {
+  $api
+    .saveManageFieldsList({ serial_no: serialNo.value, fieldsList: manageFieldsColumns.value })
+    .then((res: any) => {
+      if (res.code === 200) {
+        emit('applyNewColumn')
+        manageFieldsVisible.value = false
+      }
+    })
+}
+
+const clearData = () => {
+  manageFieldsColumns.value = []
+}
+
+defineExpose({
+  openDialog
+})
+</script>
+<template>
+  <el-dialog
+    title="Manage Report Fields"
+    v-model="manageFieldsVisible"
+    :show-close="false"
+    footer-class="manage-footer-class"
+    width="800px"
+    @closed="clearData"
+  >
+    <div class="flex">
+      <div class="flex">
+        <el-button style="height: 32px; padding: 0 13px" type="default" @click="ShowAll('show')"
+          >Show All</el-button
+        >
+        <el-button style="height: 32px; padding: 0 13px" type="default" @click="ShowAll('hide')"
+          >Hide All</el-button
+        >
+      </div>
+      <div class="fields-title">
+        {{ curSelectedNumber }} of {{ manageFieldsColumns.length }} Fields Visible
+      </div>
+    </div>
+    <div class="system-list">
+      <div class="system-list-one"></div>
+      <div class="system-list-two"></div>
+      <div class="system-list-name">System Name</div>
+      <div class="system-list-display-name">Display Name in Report</div>
+      <div class="system-list-icon"></div>
+    </div>
+    <VueDraggable
+      v-vloading="loading"
+      v-model="manageFieldsColumns"
+      class="column-list"
+      ghost-class="ghost-column"
+      :forceFallback="true"
+      fallback-class="fallback-class"
+    >
+      <template v-for="item in manageFieldsColumns" :key="item.id">
+        <div class="column-item">
+          <div class="system-list-one">
+            <span class="font_family icon-icon_dragsort__b draggable-icon"></span>
+          </div>
+          <div class="system-list-two">
+            <el-checkbox v-model="item.is_enabled"></el-checkbox>
+          </div>
+          <div class="system-list-name" :class="{ 'hide-class': !item.is_enabled }">
+            {{ item.field_display_name }}
+          </div>
+          <div class="system-list-display-name">
+            <el-input v-model="item.field_display_name_user"></el-input>
+          </div>
+          <div class="system-list-icon">
+            <span
+              class="font_family icon-icon_moveup_b move-icon"
+              style="margin-right: 16px"
+              @click="handleMoveUpSelect(item)"
+            ></span>
+            <span
+              class="font_family icon-icon_movedown_b move-icon"
+              @click="handleMoveDownSelect(item)"
+            ></span>
+          </div>
+        </div>
+      </template>
+    </VueDraggable>
+    <template #footer>
+      <el-button
+        type="default"
+        style="height: 40px; padding: 8px 40px"
+        @click="manageFieldsVisible = false"
+        >Cancel</el-button
+      >
+      <el-button
+        class="el-button--dark"
+        style="height: 40px; padding: 8px 40px"
+        @click="handleApply"
+      >
+        Apply
+      </el-button>
+    </template>
+  </el-dialog>
+</template>
+
+<style lang="scss" scoped>
+.flex {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+.fields-title {
+  font-size: 12px;
+  color: var(--color-neutral-2);
+  font-weight: 400;
+}
+.system-list {
+  background-color: var(--color-header-bg);
+  height: 24px;
+  display: flex;
+  align-items: center;
+  border-radius: 6px;
+  margin: 10px 0 0 0;
+  color: var(--color-neutral-2);
+  .system-list-name,
+  .system-list-display-name {
+    font-size: 12px;
+    color: var(--color-neutral-2);
+    font-weight: 400;
+  }
+}
+.system-list-one {
+  width: 40px;
+}
+.system-list-two {
+  width: 32px;
+}
+.system-list-name {
+  width: 28%;
+}
+.system-list-display-name {
+  width: 50%;
+}
+.system-list-icon {
+  width: 10%;
+  display: flex;
+  align-items: center;
+  justify-content: end;
+}
+.column-list {
+  height: 400px;
+  overflow: auto;
+  padding-right: 8px;
+  .column-item {
+    display: flex;
+    align-items: center;
+    height: 40px;
+    border-radius: 6px;
+    border: 1px solid var(--color-border);
+    background-color: var(--color-table-header-bg);
+    margin: 4px 0;
+    user-select: none;
+    .system-list-one {
+      display: flex;
+      align-items: center;
+      justify-content: center;
+    }
+    .system-list-name {
+      font-size: 14px;
+      font-weight: 400;
+      color: var(--color-neutral-1);
+    }
+    .hide-class {
+      text-decoration: line-through;
+      color: var(--color-neutral-2);
+    }
+    .system-list-display-name {
+      :deep(.el-input__wrapper) {
+        background-color: var(--color-table-header-bg);
+        border-radius: 6px;
+      }
+    }
+    span.draggable-icon {
+      color: var(--color-customize-column-item-drag-icon);
+    }
+    .font_family {
+      font-size: 16px;
+      cursor: pointer;
+    }
+    .move-icon {
+      &:hover {
+        color: var(--color-theme);
+      }
+    }
+  }
+}
+.ghost-column {
+  cursor: move !important;
+  span {
+    opacity: 0;
+  }
+  border: 1px dashed var(--color-customize-column-item-drag-border) !important;
+  background-color: var(--color-customize-column-item-drag-bg) !important;
+  box-shadow: none !important;
+}
+.fallback-class {
+  opacity: 1 !important;
+  background-color: var(--color-customize-column-item-hover-bg) !important;
+  cursor: move !important;
+}
+</style>

BIN
src/views/Report/src/components/ReportDetail/src/images/default_no_data@2x.png


+ 1 - 0
src/views/Report/src/components/ReportSchedule/index.ts

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

+ 194 - 0
src/views/Report/src/components/ReportSchedule/src/ReportSchedule.vue

@@ -0,0 +1,194 @@
+<script setup lang="ts">
+import ValidityPeriod from './components/ValidityPeriod.vue'
+import TimeRange from './components/TimeRange.vue'
+import EmailConfiguration from './components/EmailConfiguration.vue'
+import FieldsTable from './components/FieldsTable.vue'
+import { useRoute, useRouter } from 'vue-router'
+
+const route = useRoute()
+const router = useRouter()
+const serialNo = route.query.id as string
+
+const fieldsTableRef = ref()
+const customStartDate = ref('')
+const customEndDate = ref('')
+const loading = ref(false)
+const pageData: any = ref({
+  validityPeriod: {
+    type: '',
+    startDate: '',
+    endDate: ''
+  },
+  timeRange: {
+    endDate: '',
+    startDate: '',
+    type: '',
+    fieldType: ''
+  },
+  deliveryFrequency: {
+    deliveryFrequency: '',
+    emailRecipients: '',
+    scheduleDetails: {},
+    timezone: ''
+  },
+  orderBy: ''
+})
+
+onMounted(() => {
+  loading.value = true
+  $api
+    .getReportScheduleData({ serial_no: serialNo })
+    .then((res: any) => {
+      if (res.code === 200) {
+        pageData.value = res.data.showData
+      }
+    })
+    .then(() => {
+      loading.value = false
+    })
+})
+
+// 提交ValidityPeriod组件数据
+const handleSubmitValidity = (data: any) => {
+  customStartDate.value = data.startDate
+  customEndDate.value = data.endDate
+}
+
+const changeTimeRange = (data: any) => {
+  const queryData = {
+    startDate: data.startDate,
+    endDate: data.endDate,
+    fieldType: data.fieldType,
+    type: data.type
+  }
+  if (data.type && (data.startDate || data.endDate) && data.fieldType) {
+    fieldsTableRef.value.handleSearch(queryData)
+  }
+}
+
+const validityPeriodRef = ref()
+const timeRangeRef = ref()
+const emailConfigurationRef = ref()
+
+const handleSave = () => {
+  const validityPeriod = validityPeriodRef.value.getData()
+  const timeRange = timeRangeRef.value.getData()
+  const emailConfiguration = emailConfigurationRef.value.getData()
+  const orderBy = fieldsTableRef.value.getData()
+  // 如果有一个返回false, 则代表有未填选项 结束保存
+  if (!validityPeriod || !timeRange || !emailConfiguration) return
+  const queryData = {
+    validityPeriodType: validityPeriod.type,
+    validityPeriodStartDate: validityPeriod.startDate,
+    validityPeriodEndDate: validityPeriod.endDate,
+    type: timeRange.type,
+    fieldType: timeRange.fieldType,
+    startDate: timeRange.startValue,
+    endDate: timeRange.endValue,
+    emailRecipients: emailConfiguration.emailRecipients,
+    deliveryFrequency: emailConfiguration.deliveryFrequency,
+    timezone: emailConfiguration.timezone,
+    ...emailConfiguration.scheduleDetails,
+    orderBy
+  }
+  $api
+    .saveReportScheduleData({
+      serial_no: serialNo,
+      ...queryData
+    })
+    .then((res: any) => {
+      if (res.code === 200) {
+        ElMessage.success('Save Success')
+        router.push({ path: '/report' })
+      }
+    })
+}
+</script>
+<template>
+  <div v-vloading="loading">
+    <div class="Title">
+      <span>Schedule Configuration - Shipment Status Report</span>
+      <el-button @click="handleSave" class="el-button--main save_button"
+        ><span class="font_family icon-icon_save_b icon_dark" style="margin-right: 5px"></span
+        >Save</el-button
+      >
+    </div>
+    <div class="container">
+      <div class="schedule_rule">
+        <div class="schedule_title">
+          <span class="stars_red">*</span>
+          Schedule Rule Validity Period
+        </div>
+        <div class="schedule_container">
+          <ValidityPeriod
+            ref="validityPeriodRef"
+            :data="pageData.validityPeriod"
+            @handleSubmitValidity="handleSubmitValidity"
+          >
+          </ValidityPeriod>
+        </div>
+      </div>
+      <div class="schedule_rule" style="margin: 8px 0">
+        <div class="schedule_title">Report Data Time Range</div>
+        <div class="schedule_container">
+          <TimeRange
+            ref="timeRangeRef"
+            :data="pageData.timeRange"
+            @changeTimeRange="changeTimeRange"
+          >
+          </TimeRange>
+        </div>
+      </div>
+      <div class="schedule_rule" style="margin: 8px 0">
+        <div class="schedule_title">Report Delivery Frequency & Email Configuration</div>
+        <div class="schedule_container">
+          <EmailConfiguration
+            ref="emailConfigurationRef"
+            :data="pageData.deliveryFrequency"
+          ></EmailConfiguration>
+        </div>
+      </div>
+      <FieldsTable :orderBy="pageData.orderBy" ref="fieldsTableRef"></FieldsTable>
+    </div>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.Title {
+  display: flex;
+  justify-content: space-between;
+  position: sticky;
+  z-index: 100;
+  top: 0;
+  padding: 0 24px;
+  align-items: center;
+  background-color: var(--color-mode);
+  height: 68px;
+  border-bottom: 1px solid var(--color-border);
+  font-size: var(--font-size-6);
+  font-weight: 700;
+}
+.save_button {
+  height: 40px;
+  padding: 8px 34px;
+}
+.container {
+  padding: 16px 24px;
+  padding-bottom: 48px;
+  .schedule_rule {
+    border: 1px solid var(--color-border);
+    border-radius: 12px;
+    .schedule_title {
+      padding: 13px 16px;
+      font-weight: 700;
+      font-size: 18px;
+      color: var(--color-neutral-1);
+      background-color: var(--color-schedule-bg);
+      border-radius: 12px 12px 0 0;
+      .stars_red {
+        color: var(--color-danger);
+      }
+    }
+  }
+}
+</style>

+ 507 - 0
src/views/Report/src/components/ReportSchedule/src/components/EmailConfiguration.vue

@@ -0,0 +1,507 @@
+<script setup lang="ts">
+import { time } from 'console'
+import moment from 'moment-timezone'
+
+const props = defineProps<{
+  data: any
+}>()
+watch(
+  () => props.data,
+  (newVal) => {
+    emailValue.value = newVal.emailRecipients
+    timezoneValue.value = newVal.timezone
+    frequencyValue.value = newVal.deliveryFrequency
+    const dateData = newVal.scheduleDetails
+    timeValue.value = dateData.time
+    weeklyChecked.value = dateData.week
+    monthlyChecked.value = dateData.monthlyDay
+    quarterMonthValue.value = dateData.quarterMonth
+    dayValue.value = dateData.day
+    yearlyMonth.value = dateData.yearlyMonth
+  }
+)
+
+const emailValue = ref('')
+const timezoneValue = ref('UTC' + moment().tz(moment.tz.guess()).format('Z'))
+const frequencyValue = ref('')
+const timeValue = ref('')
+const generateTimeOptions = (intervalMinutes = 30): Array<{ value: string; label: string }> => {
+  const options = []
+  const totalMinutesInDay = 24 * 60 // 1440
+
+  for (let i = 0; i < totalMinutesInDay; i += intervalMinutes) {
+    const hours = String(Math.floor(i / 60)).padStart(2, '0')
+    const minutes = String(i % 60).padStart(2, '0')
+    const timeStr = `${hours}:${minutes}`
+    options.push({
+      value: timeStr,
+      label: timeStr
+    })
+  }
+  return options
+}
+
+const timeItem = generateTimeOptions(30)
+
+const timezoneoptions = ref([
+  {
+    label: 'UTC-08',
+    value: 'UTC-08:00'
+  },
+  {
+    label: 'UTC-07',
+    value: 'UTC-07:00'
+  },
+  {
+    label: 'UTC-06',
+    value: 'UTC-06:00'
+  },
+  {
+    label: 'UTC-05',
+    value: 'UTC-05:00'
+  },
+  {
+    label: 'UTC-04',
+    value: 'UTC-04:00'
+  },
+  {
+    label: 'UTC-03',
+    value: 'UTC-03:00'
+  },
+  {
+    label: 'UTC-02',
+    value: 'UTC-02:00'
+  },
+  {
+    label: 'UTC-01',
+    value: 'UTC-01:00'
+  },
+  {
+    label: 'UTC-00',
+    value: 'UTC-00:00'
+  },
+  {
+    label: 'UTC+01',
+    value: 'UTC+01:00'
+  },
+  {
+    label: 'UTC+02',
+    value: 'UTC+02:00'
+  },
+  {
+    label: 'UTC+03',
+    value: 'UTC+03:00'
+  },
+  {
+    label: 'UTC+04',
+    value: 'UTC+04:00'
+  },
+  {
+    label: 'UTC+05',
+    value: 'UTC+05:00'
+  },
+  {
+    label: 'UTC+05:30',
+    value: 'UTC+05:30'
+  },
+  {
+    label: 'UTC+06',
+    value: 'UTC+06:00'
+  },
+  {
+    label: 'UTC+07',
+    value: 'UTC+07:00'
+  },
+  {
+    label: 'UTC+08',
+    value: 'UTC+08:00'
+  },
+  {
+    label: 'UTC+09',
+    value: 'UTC+09:00'
+  },
+  {
+    label: 'UTC+10',
+    value: 'UTC+10:00'
+  },
+  {
+    label: 'UTC+11',
+    value: 'UTC+11:00'
+  },
+  {
+    label: 'UTC+12',
+    value: 'UTC+12:00'
+  }
+])
+
+const frequencyOptions = ref([
+  {
+    label: 'Daily',
+    value: 'daily'
+  },
+  {
+    label: 'Weekly',
+    value: 'weekly'
+  },
+  {
+    label: 'Monthly',
+    value: 'monthly'
+  },
+  {
+    label: 'Quarterly',
+    value: 'quarterly'
+  },
+  {
+    label: 'Yearly',
+    value: 'yearly'
+  }
+])
+
+const weeklyCheckList = ref([
+  'Monday',
+  'Tuesday',
+  'Wednesday',
+  'Thursday',
+  'Friday',
+  'Saturday',
+  'Sunday'
+])
+const weeklyChecked = ref([])
+
+const monthlyChecked = ref([])
+
+const yearlyCheckList = ref([
+  'January',
+  'February',
+  'March',
+  'April',
+  'May',
+  'June',
+  'July',
+  'August',
+  'September',
+  'October',
+  'November',
+  'December'
+])
+
+const yearlyMonth = ref([])
+const dayValue = ref('')
+
+const quarterMonth = ref(['1st Month', '2nd Month', '3rd Month'])
+const quarterMonthValue = ref('')
+
+// 切换frequency时,清空内容
+const handleClickFrequency = () => {
+  weeklyChecked.value = []
+  monthlyChecked.value = []
+  yearlyMonth.value = []
+  timeValue.value = ''
+  quarterMonthValue.value = ''
+  dayValue.value = ''
+}
+
+const getData = () => {
+  if (!frequencyValue.value || !emailValue.value || !timezoneValue.value) {
+    ElMessage.warning('Please select the Report Delivery Frequency & Email Configuration')
+    return false
+  }
+  if (
+    (frequencyValue.value === 'daily' && !timeValue.value) ||
+    (frequencyValue.value === 'weekly' && (weeklyChecked.value.length === 0 || !timeValue.value)) ||
+    (frequencyValue.value === 'monthly' &&
+      (monthlyChecked.value.length === 0 || !timeValue.value)) ||
+    (frequencyValue.value === 'quarterly' &&
+      (!quarterMonthValue.value || !dayValue.value || !timeValue.value)) ||
+    (frequencyValue.value === 'yearly' &&
+      (yearlyMonth.value.length === 0 || !dayValue.value || !timeValue.value))
+  ) {
+    ElMessage.error('Please select the Report Delivery Frequency & Email Configuration')
+    return false
+  }
+  return {
+    emailRecipients: emailValue.value,
+    timezone: timezoneValue.value,
+    deliveryFrequency: frequencyValue.value,
+    scheduleDetails: {
+      time: timeValue.value,
+      week: weeklyChecked.value,
+      monthlyDay: monthlyChecked.value,
+      quarterMonth: quarterMonthValue.value,
+      day: dayValue.value,
+      yearlyMonth: yearlyMonth.value
+    }
+  }
+}
+
+defineExpose({
+  getData
+})
+</script>
+<template>
+  <div style="padding: 8px 16px 0 16px">
+    <div class="title">
+      <span class="stars_red">*</span>
+      Email Recipients
+    </div>
+    <el-input
+      v-model="emailValue"
+      :rows="4"
+      type="textarea"
+      placeholder="Enter email address separated by commas"
+      style="margin-top: 4px"
+    ></el-input>
+  </div>
+  <div class="line"></div>
+  <div style="padding: 0 16px">
+    <div class="flex">
+      <div class="timezone" style="margin-right: 8px">
+        <div class="title">
+          <span class="stars_red">*</span>
+          Timezone
+        </div>
+        <el-select class="select_time" v-model="timezoneValue" placeholder="Please select...">
+          <el-option
+            v-for="item in timezoneoptions"
+            :key="item.value"
+            :label="item.label"
+            :value="item.value"
+          />
+        </el-select>
+      </div>
+      <div class="timezone">
+        <div class="title">
+          <span class="stars_red">*</span>
+          Delivery Frequency
+        </div>
+        <el-select
+          class="select_time"
+          v-model="frequencyValue"
+          placeholder="Please select..."
+          @change="handleClickFrequency"
+        >
+          <el-option
+            v-for="item in frequencyOptions"
+            :key="item.value"
+            :label="item.label"
+            :value="item.value"
+          />
+        </el-select>
+      </div>
+    </div>
+    <div class="schedule_details">
+      <div class="detail_title">Schedule Details</div>
+      <div class="weelkly_detailes" v-if="frequencyValue == 'weekly'">
+        <div class="title">
+          <span class="stars_red">*</span>
+          Days of Week (Select multiple)
+        </div>
+        <el-checkbox-group v-model="weeklyChecked" style="margin: 4px 0 16px 0">
+          <el-checkbox
+            border
+            v-for="(item, index) in weeklyCheckList"
+            :key="item"
+            :value="index + 1"
+          >
+            {{ item }}
+          </el-checkbox>
+        </el-checkbox-group>
+      </div>
+      <div class="monthly_detailes" v-else-if="frequencyValue == 'monthly'">
+        <div class="title">
+          <span class="stars_red">*</span>
+          Days of Month (Select multiple)
+        </div>
+        <el-checkbox-group v-model="monthlyChecked" style="margin: 4px 0 16px 0">
+          <el-checkbox border v-for="item in 31" :key="item" :value="item">
+            {{ item }}
+          </el-checkbox>
+        </el-checkbox-group>
+      </div>
+      <div class="yearly_detailes" v-else-if="frequencyValue == 'yearly'">
+        <div class="title">
+          <span class="stars_red">*</span>
+          Months (Select multiple)
+        </div>
+        <el-checkbox-group v-model="yearlyMonth" style="margin: 4px 0 16px 0">
+          <el-checkbox
+            border
+            v-for="(item, index) in yearlyCheckList"
+            :key="item"
+            :value="index + 1"
+          >
+            {{ item }}
+          </el-checkbox>
+        </el-checkbox-group>
+      </div>
+      <div class="schedule-details-selects" style="display: flex">
+        <div class="details_time" v-if="frequencyValue == 'quarterly'">
+          <div class="title">
+            <span class="stars_red">*</span>
+            Quarter Month
+          </div>
+          <el-select
+            style="margin: 4px 8px 0 0"
+            v-model="quarterMonthValue"
+            placeholder="Please select..."
+          >
+            <el-option
+              v-for="(item, index) in quarterMonth"
+              :key="item"
+              :label="item"
+              :value="index + 1"
+            />
+          </el-select>
+        </div>
+        <div
+          class="details_time"
+          v-if="frequencyValue == 'yearly' || frequencyValue == 'quarterly'"
+        >
+          <div class="title">
+            <span class="stars_red">*</span>
+            Day
+          </div>
+          <el-select style="margin: 4px 8px 0 0" v-model="dayValue" placeholder="Please select...">
+            <el-option v-for="item in 31" :key="item" :label="item" :value="item" />
+          </el-select>
+        </div>
+        <div class="details_time">
+          <div class="title">
+            <span class="stars_red">*</span>
+            Time
+          </div>
+
+          <el-select
+            v-model="timeValue"
+            style="width: 100%; margin: 4px 0 0 0"
+            :style="{
+              maxWidth:
+                frequencyValue === 'daily' ||
+                frequencyValue === 'weekly' ||
+                frequencyValue === 'monthly' ||
+                !frequencyValue
+                  ? '472px'
+                  : '100%'
+            }"
+            placeholder="Please select..."
+          >
+            <el-option
+              v-for="item in timeItem"
+              :key="item.value"
+              :label="item.label"
+              :value="item.value"
+            />
+          </el-select>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.title {
+  color: var(--color-neutral-2);
+  font-size: 12px;
+  font-weight: 400;
+}
+.stars_red {
+  color: var(--color-danger);
+}
+:deep(.el-textarea) {
+  .el-textarea__inner {
+    resize: none; // 去除右下角图标
+    padding: 5px 7px 5px 10px;
+    box-shadow: 0 0 0 1px var(--color-select-border) inset;
+  }
+}
+.line {
+  margin: 16px 0;
+  border-bottom: 1px solid var(--color-border);
+}
+.flex {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+.timezone {
+  width: 50%;
+}
+.select_time {
+  margin-top: 4px;
+}
+.schedule_details {
+  background-color: var(--color-schedule-details-bg);
+  border-radius: 6px;
+  margin: 8px 0 20px 0;
+  padding: 11px 0 8px 8px;
+  .detail_title {
+    color: var(--color-neutral-1);
+    font-weight: 700;
+    font-size: 14px;
+    margin-bottom: 20px;
+  }
+}
+.schedule-details-selects {
+  display: flex;
+  gap: 8px;
+  width: 100%;
+  padding-right: 8px;
+  .details_time {
+    flex: 1;
+  }
+}
+
+:deep(.el-checkbox.is-bordered) {
+  width: 14.2%;
+  margin-right: 0;
+  border-radius: 0;
+  border: 1px solid var(--color-select-border);
+  background-color: var(--management-bg-color);
+  height: 40px;
+}
+:deep(.el-checkbox.is-bordered:first-child) {
+  border-radius: 6px 0 0 6px;
+}
+:deep(.el-checkbox.is-bordered:last-child) {
+  border-radius: 0 6px 6px 0;
+}
+:deep(.monthly_detailes .el-checkbox.is-bordered:first-child) {
+  border-radius: 6px 0 0 0;
+}
+:deep(.monthly_detailes .el-checkbox.is-bordered:nth-child(7)) {
+  border-radius: 0 6px 0 0;
+}
+:deep(.monthly_detailes .el-checkbox.is-bordered:nth-child(28)) {
+  border-radius: 0 0 6px 0;
+}
+:deep(.monthly_detailes .el-checkbox.is-bordered:nth-child(29)) {
+  border-radius: 0 0 0 6px;
+}
+:deep(.february_class .el-checkbox.is-bordered:nth-child(29)) {
+  border-radius: 0 0 0 0;
+}
+:deep(.february_class .el-checkbox.is-bordered:nth-child(22)) {
+  border-radius: 0 0 0 6px;
+}
+:deep(.monthly_detailes .el-checkbox.is-bordered:last-child) {
+  border-radius: 0 0 6px 0;
+}
+:deep(.nine_class .el-checkbox.is-bordered:last-child) {
+  border-radius: 0 0 6px 6px;
+}
+:deep(.yearly_detailes .el-checkbox.is-bordered) {
+  width: 16.6%;
+}
+:deep(.yearly_detailes .el-checkbox.is-bordered:first-child) {
+  border-radius: 6px 0 0 0;
+}
+:deep(.yearly_detailes .el-checkbox.is-bordered:nth-child(6)) {
+  border-radius: 0 6px 0 0;
+}
+:deep(.yearly_detailes .el-checkbox.is-bordered:nth-child(7)) {
+  border-radius: 0 0 0 6px;
+}
+:deep(.yearly_detailes .el-checkbox.is-bordered:last-child) {
+  border-radius: 0 0 6px 0;
+}
+</style>

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

@@ -0,0 +1,253 @@
+<script setup lang="ts">
+import { ref, onMounted } from 'vue'
+import { type VxeGridInstance, type VxeGridProps } from 'vxe-table'
+import { useRowClickStyle } from '@/hooks/rowClickStyle'
+import { formatNumber } from '@/utils/tools'
+import { autoWidth } from '@/utils/table'
+import ManageReportFields from './ManageReportFields.vue'
+import { useRoute } from 'vue-router'
+
+const route = useRoute()
+
+const orderBy = ref('')
+const props = defineProps<{
+  orderBy: any
+}>()
+watch(
+  () => props.orderBy,
+  (newVal) => (orderBy.value = newVal)
+)
+
+const manageReportFieldsRef = ref()
+const columnSortoptions = [
+  {
+    label: 'ETA',
+    value: 'eta'
+  },
+  {
+    label: 'ETD',
+    value: 'etd'
+  }
+]
+
+const tableLoading = ref(false)
+
+const tableData = ref<VxeGridProps<any>>({
+  border: true,
+  round: true,
+  columns: [],
+  data: [],
+  scrollY: { enabled: true, oSize: 20, gt: 30 },
+  stripe: true,
+  emptyText: ' ',
+  showHeaderOverflow: true,
+  showOverflow: true,
+  headerRowStyle: {
+    backgroundColor: 'var(--color-table-header-bg)'
+  },
+  columnConfig: { resizable: true, useKey: true },
+  rowConfig: { isHover: true },
+  exportConfig: {
+    types: ['csv', 'html', 'txt', 'xlsx'],
+    modes: ['current', 'selected', 'all']
+  }
+})
+
+const tableRef = ref<VxeGridInstance | null>(null)
+const pageInfo = ref({ pageNo: 1, pageSize: 50, total: 0 })
+
+const handleColumns = (columns: any) => {
+  const newColumns = columns.map((item: any) => {
+    let curColumn: any = {
+      title: item.title,
+      field: item.field,
+      width: item.width
+    }
+    // 设置插槽
+    if (item.type === 'status') {
+      curColumn = {
+        ...curColumn,
+        slots: { default: 'status' }
+      }
+    }
+    return curColumn
+  })
+  return newColumns
+}
+
+// 获取表格数据
+const getTableData = (isPageChange?: boolean) => {
+  tableLoading.value = true
+  $api
+    .getReportScheduleTable({
+      cp: pageInfo.value.pageNo,
+      ps: pageInfo.value.pageSize,
+      rc: isPageChange ? pageInfo.value.total : -1,
+      serial_no: route.query.id,
+      orderBy: orderBy.value,
+      ...queryParams.value
+    })
+    .then((res: any) => {
+      if (res.code === 200) {
+        pageInfo.value.total = Number(res.data.rc)
+        pageInfo.value.pageNo = res.data.cp
+        pageInfo.value.pageSize = res.data.ps
+
+        const tableDataValue = res.data.tableData
+        pageInfo.value.total = Number(tableDataValue.rc)
+        pageInfo.value.pageNo = tableDataValue.cp
+        pageInfo.value.pageSize = tableDataValue.ps
+        tableData.value.data = tableDataValue.searchData
+
+        tableData.value.columns = handleColumns(res.data.tableColumns)
+      }
+    })
+    .finally(() => {
+      nextTick(() => {
+        tableRef.value && autoWidth(tableData.value, tableRef.value)
+        tableLoading.value = false
+      })
+    })
+}
+
+const queryParams = ref({
+  serial_no: route.query.id
+})
+// 查询时调用接口
+const handleSearch = (val: any) => {
+  queryParams.value = val
+  if (!orderBy.value) {
+    orderBy.value = val.fieldType
+  }
+
+  getTableData()
+}
+
+const handleClickManageFields = () => {
+  manageReportFieldsRef.value.openDialog(route.query.id as string)
+}
+
+const applyNewColumn = () => {
+  getTableData()
+}
+
+// 实现行点击样式
+useRowClickStyle(tableRef)
+
+const getData = () => {
+  return orderBy.value
+}
+
+defineExpose({
+  handleSearch,
+  getData
+})
+</script>
+<template>
+  <div>
+    <div class="SettingTable">
+      <div class="flex">
+        <div class="title">Report Data Review</div>
+        <div class="flex">
+          <span class="sort-text">Sort by:</span>
+          <el-select
+            style="width: 160px; margin: 0 8px"
+            v-model="orderBy"
+            placeholder="Please select..."
+          >
+            <el-option
+              v-for="item in columnSortoptions"
+              :key="item.value"
+              :label="item.label"
+              :value="item.value"
+            />
+          </el-select>
+          <el-button type="default" @click="handleClickManageFields">
+            <span class="font_family icon-icon_set_b"></span>Manage Fields
+          </el-button>
+          <ManageReportFields
+            ref="manageReportFieldsRef"
+            @applyNewColumn="applyNewColumn"
+          ></ManageReportFields>
+        </div>
+      </div>
+      <vxe-grid
+        ref="tableRef"
+        :style="{ border: 'none' }"
+        v-bind="tableData"
+        height="450"
+        v-vloading="tableLoading"
+      >
+        <!-- 空数据时的插槽 -->
+        <template #empty>
+          <img src="../../../ReportDetail/src/images/default_no_data@2x.png" />
+          <div class="empty-text">No data</div>
+        </template>
+        <!-- Status字段的插槽 -->
+        <template #status="{ row, column }">
+          <VTag :type="row[column.field]">{{ row[column.field] }}</VTag>
+        </template>
+      </vxe-grid>
+      <div class="pagination">
+        <span>Total {{ formatNumber(pageInfo.total) }}</span>
+        <el-pagination
+          v-model:current-page="pageInfo.pageNo"
+          v-model:page-size="pageInfo.pageSize"
+          :page-sizes="[50, 100, 200, 300, 400]"
+          :pagerCount="5"
+          background
+          layout="sizes, prev, pager, next"
+          :total="pageInfo.total"
+          @size-change="getTableData(true)"
+          @current-change="getTableData(true)"
+        />
+      </div>
+    </div>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.flex {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  margin-bottom: 13px;
+  .sort-text {
+    font-size: 12px;
+    font-weight: 400;
+    color: var(--color-neutral-2);
+  }
+}
+.title {
+  font-size: 18px;
+  font-weight: 700;
+}
+.pagination {
+  display: flex;
+  justify-content: space-between;
+  font-weight: 400;
+  font-size: 15px;
+  align-items: center;
+  border: 1px solid var(--color-border);
+  border-top: none;
+  padding: 4px 8px;
+  border-radius: 0 0 6px 6px;
+}
+:deep(.el-icon svg) {
+  width: 1em !important;
+}
+.SettingTable {
+  padding: 13px 24px 24px 24px;
+  font-weight: 400;
+  border: 1px solid var(--color-border);
+  border-radius: 12px;
+  border-top: 1px solid var(--color-border);
+}
+.empty-text {
+  margin: 8px 0;
+  color: var(--color-neutral-2);
+  text-align: center;
+  font-size: 14px;
+  font-weight: 700;
+}
+</style>

+ 261 - 0
src/views/Report/src/components/ReportSchedule/src/components/ManageReportFields.vue

@@ -0,0 +1,261 @@
+<script setup lang="ts">
+import { VueDraggable } from 'vue-draggable-plus'
+interface ColumnItem {
+  id: string
+  field_display_name: string
+  is_enabled: boolean
+  field_display_name_user: string
+}
+
+const manageFieldsVisible = ref(false)
+const manageFieldsColumns = ref<ColumnItem[]>([])
+const loading = ref(false)
+const serialNo = ref('')
+const openDialog = (id: string) => {
+  loading.value = true
+  manageFieldsVisible.value = true
+  serialNo.value = id
+  $api
+    .getManageFieldsList({ serial_no: id })
+    .then((res: any) => {
+      if (res.code === 200) {
+        manageFieldsColumns.value = res.data.data
+      }
+    })
+    .finally(() => {
+      loading.value = false
+    })
+}
+const curSelectedNumber = computed(() => {
+  if (!manageFieldsColumns.value) return 0
+  return manageFieldsColumns.value.filter((item: any) => item.is_enabled).length
+})
+const handleMoveUpSelect = (data: any) => {
+  const index = manageFieldsColumns.value.findIndex((item: any) => item.id === data.id)
+  if (index === 0) return
+  const temp = manageFieldsColumns.value[index]
+  manageFieldsColumns.value[index] = manageFieldsColumns.value[index - 1]
+  manageFieldsColumns.value[index - 1] = temp
+}
+const handleMoveDownSelect = (data: any) => {
+  const index = manageFieldsColumns.value.findIndex((item: any) => item.id === data.id)
+  if (index === manageFieldsColumns.value.length - 1) return
+  const temp = manageFieldsColumns.value[index]
+  manageFieldsColumns.value[index] = manageFieldsColumns.value[index + 1]
+  manageFieldsColumns.value[index + 1] = temp
+}
+
+const ShowAll = (type: string) => {
+  manageFieldsColumns.value.forEach((item: any) => {
+    item.is_enabled = type === 'show'
+  })
+}
+const emit = defineEmits(['applyNewColumn'])
+const handleApply = () => {
+  $api
+    .saveManageFieldsList({ serial_no: serialNo.value, fieldsList: manageFieldsColumns.value })
+    .then((res: any) => {
+      if (res.code === 200) {
+        emit('applyNewColumn')
+        manageFieldsVisible.value = false
+      }
+    })
+}
+
+const clearData = () => {
+  manageFieldsColumns.value = []
+}
+
+defineExpose({
+  openDialog
+})
+</script>
+<template>
+  <el-dialog
+    title="Manage Report Fields"
+    v-model="manageFieldsVisible"
+    :show-close="false"
+    footer-class="manage-footer-class"
+    width="800px"
+    @closed="clearData"
+  >
+    <div class="flex">
+      <div class="flex">
+        <el-button style="height: 32px; padding: 0 13px" type="default" @click="ShowAll('show')"
+          >Show All</el-button
+        >
+        <el-button style="height: 32px; padding: 0 13px" type="default" @click="ShowAll('hide')"
+          >Hide All</el-button
+        >
+      </div>
+      <div class="fields-title">
+        {{ curSelectedNumber }} of {{ manageFieldsColumns.length }} Fields Visible
+      </div>
+    </div>
+    <div class="system-list">
+      <div class="system-list-one"></div>
+      <div class="system-list-two"></div>
+      <div class="system-list-name">System Name</div>
+      <div class="system-list-display-name">Display Name in Report</div>
+      <div class="system-list-icon"></div>
+    </div>
+    <VueDraggable
+      v-vloading="loading"
+      v-model="manageFieldsColumns"
+      class="column-list"
+      ghost-class="ghost-column"
+      :forceFallback="true"
+      fallback-class="fallback-class"
+    >
+      <template v-for="item in manageFieldsColumns" :key="item.id">
+        <div class="column-item">
+          <div class="system-list-one">
+            <span class="font_family icon-icon_dragsort__b draggable-icon"></span>
+          </div>
+          <div class="system-list-two">
+            <el-checkbox v-model="item.is_enabled"></el-checkbox>
+          </div>
+          <div class="system-list-name" :class="{ 'hide-class': !item.is_enabled }">
+            {{ item.field_display_name }}
+          </div>
+          <div class="system-list-display-name">
+            <el-input v-model="item.field_display_name_user"></el-input>
+          </div>
+          <div class="system-list-icon">
+            <span
+              class="font_family icon-icon_moveup_b move-icon"
+              style="margin-right: 16px"
+              @click="handleMoveUpSelect(item)"
+            ></span>
+            <span
+              class="font_family icon-icon_movedown_b move-icon"
+              @click="handleMoveDownSelect(item)"
+            ></span>
+          </div>
+        </div>
+      </template>
+    </VueDraggable>
+    <template #footer>
+      <el-button
+        type="default"
+        style="height: 40px; padding: 8px 40px"
+        @click="manageFieldsVisible = false"
+        >Cancel</el-button
+      >
+      <el-button
+        class="el-button--dark"
+        style="height: 40px; padding: 8px 40px"
+        @click="handleApply"
+      >
+        Apply
+      </el-button>
+    </template>
+  </el-dialog>
+</template>
+
+<style lang="scss" scoped>
+.flex {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+.fields-title {
+  font-size: 12px;
+  color: var(--color-neutral-2);
+  font-weight: 400;
+}
+.system-list {
+  background-color: var(--color-header-bg);
+  height: 24px;
+  display: flex;
+  align-items: center;
+  border-radius: 6px;
+  margin: 10px 0 0 0;
+  color: var(--color-neutral-2);
+  .system-list-name,
+  .system-list-display-name {
+    font-size: 12px;
+    color: var(--color-neutral-2);
+    font-weight: 400;
+  }
+}
+.system-list-one {
+  width: 40px;
+}
+.system-list-two {
+  width: 32px;
+}
+.system-list-name {
+  width: 28%;
+}
+.system-list-display-name {
+  width: 50%;
+}
+.system-list-icon {
+  width: 10%;
+  display: flex;
+  align-items: center;
+  justify-content: end;
+}
+.column-list {
+  height: 400px;
+  overflow: auto;
+  padding-right: 8px;
+  .column-item {
+    display: flex;
+    align-items: center;
+    height: 40px;
+    border-radius: 6px;
+    border: 1px solid var(--color-border);
+    background-color: var(--color-table-header-bg);
+    margin: 4px 0;
+    user-select: none;
+    .system-list-one {
+      display: flex;
+      align-items: center;
+      justify-content: center;
+    }
+    .system-list-name {
+      font-size: 14px;
+      font-weight: 400;
+      color: var(--color-neutral-1);
+    }
+    .hide-class {
+      text-decoration: line-through;
+      color: var(--color-neutral-2);
+    }
+    .system-list-display-name {
+      :deep(.el-input__wrapper) {
+        background-color: var(--color-table-header-bg);
+        border-radius: 6px;
+      }
+    }
+    span.draggable-icon {
+      color: var(--color-customize-column-item-drag-icon);
+    }
+    .font_family {
+      font-size: 16px;
+      cursor: pointer;
+    }
+    .move-icon {
+      &:hover {
+        color: var(--color-theme);
+      }
+    }
+  }
+}
+.ghost-column {
+  cursor: move !important;
+  span {
+    opacity: 0;
+  }
+  border: 1px dashed var(--color-customize-column-item-drag-border) !important;
+  background-color: var(--color-customize-column-item-drag-bg) !important;
+  box-shadow: none !important;
+}
+.fallback-class {
+  opacity: 1 !important;
+  background-color: var(--color-customize-column-item-hover-bg) !important;
+  cursor: move !important;
+}
+</style>

+ 526 - 0
src/views/Report/src/components/ReportSchedule/src/components/TimeRange.vue

@@ -0,0 +1,526 @@
+<script setup lang="ts">
+import { useUserStore } from '@/stores/modules/user'
+import dayjs from 'dayjs'
+
+const userStore = useUserStore()
+const fieldType = ref('')
+const startDate = ref('')
+const endDate = ref('')
+const rollingValueStart = ref(0)
+const rollingValueEnd = ref(0)
+const type = ref('')
+const fixedRangeRadio = ref('Customize')
+const rollingRangeRadio = ref('Customize')
+const dataTimeoptions = ref([
+  {
+    label: 'ETD',
+    value: 'etd'
+  },
+  {
+    label: 'ETA',
+    value: 'eta'
+  }
+])
+
+const props = defineProps<{
+  data: any
+}>()
+watch(
+  () => props.data,
+  (newVal) => {
+    fieldType.value = newVal.fieldType
+    type.value = newVal.type
+    if (type.value === 'dynamic_rolling') {
+      rollingValueStart.value = newVal.startDate
+      rollingValueEnd.value = newVal.endDate
+    } else if (type.value === 'fixed') {
+      startDate.value = dayjs(newVal.startDate, 'YYYY-MM-DD', true).format('MM/DD/YYYY')
+      endDate.value = dayjs(newVal.endDate, 'YYYY-MM-DD', true).format('MM/DD/YYYY')
+    }
+  }
+)
+const isShowRolling = computed(() => type.value === 'dynamic_rolling')
+const isShowFixed = computed(() => type.value === 'fixed')
+
+const clampedValueStart = computed({
+  get: () => rollingValueStart.value,
+  set: (newVal: any) => {
+    // 转换为整数
+    const num = parseInt(newVal, 10)
+    // 处理非数字和NaN情况
+    if (isNaN(num)) {
+      rollingValueStart.value = 0
+      return 0
+    }
+    // 范围限制
+    rollingValueStart.value = Math.max(0, Math.min(365, num))
+  }
+})
+const clampedValueEnd = computed({
+  get: () => rollingValueEnd.value,
+  set: (newVal: any) => {
+    // 转换为整数
+    const num = parseInt(newVal, 10)
+    // 处理非数字和NaN情况
+    if (isNaN(num)) {
+      rollingValueEnd.value = 0
+      return 0
+    }
+    // 范围限制
+    rollingValueEnd.value = Math.max(0, Math.min(365, num))
+  }
+})
+
+const disabledDate = (date: any) => {
+  if (!startDate.value) return false
+  const currentDate = dayjs(date).format('YYYY-MM-DD')
+  return dayjs(currentDate).isBefore(startDate.value)
+}
+
+const disabledDateEnd = (date: any) => {
+  if (!endDate.value) return false
+  const currentDate = dayjs(date).format('YYYY-MM-DD')
+  return dayjs(currentDate).isAfter(endDate.value)
+}
+
+const emits = defineEmits<{
+  (e: 'changeTimeRange', val: any): void
+}>()
+// 输入时间
+const changeTime = (val: any) => {
+  if (val === 'dynamic_rolling') {
+    startDate.value = ''
+    endDate.value = ''
+
+    rollingRangeRadio.value = 'Customize'
+    emits('changeTimeRange', {
+      startDate: clampedValueStart.value,
+      endDate: clampedValueEnd.value,
+      fieldType: fieldType.value,
+      type: type.value
+    })
+  } else if (val === 'fixed') {
+    clampedValueStart.value = 0
+    clampedValueEnd.value = 0
+
+    fixedRangeRadio.value = 'Customize'
+    emits('changeTimeRange', {
+      startDate: startDate.value,
+      endDate: endDate.value,
+      fieldType: fieldType.value,
+      type: type.value
+    })
+  }
+}
+// 选择Dynamic Rolling Range默认值
+const ChangeRollingRangeRadio = (val: any) => {
+  if (val == 'Next 30 days') {
+    clampedValueStart.value = 0
+    clampedValueEnd.value = 30
+  } else if (val == 'Next 60 days') {
+    clampedValueStart.value = 0
+    clampedValueEnd.value = 60
+  } else if (val == 'Past 30 days') {
+    clampedValueStart.value = 30
+    clampedValueEnd.value = 0
+  } else if (val == 'Past 10 days to next 60 days') {
+    clampedValueStart.value = 10
+    clampedValueEnd.value = 60
+  } else {
+    clampedValueStart.value = 0
+    clampedValueEnd.value = 0
+  }
+
+  emits('changeTimeRange', {
+    startDate: clampedValueStart.value,
+    endDate: clampedValueEnd.value,
+    fieldType: fieldType.value,
+    type: type.value
+  })
+}
+
+// 选择fixed range默认值
+const changeFixedRange = (val: any) => {
+  if (val == 'This Month') {
+    startDate.value = dayjs().startOf('month').format('MM/DD/YYYY')
+    endDate.value = dayjs().endOf('month').format('MM/DD/YYYY')
+  } else if (val == 'Last Month') {
+    startDate.value = dayjs().subtract(1, 'month').startOf('month').format('MM/DD/YYYY')
+    endDate.value = dayjs().subtract(1, 'month').endOf('month').format('MM/DD/YYYY')
+  } else if (val == 'This Quarter') {
+    startDate.value = dayjs()
+      .month(Math.floor(dayjs().month() / 3) * 3)
+      .startOf('month')
+      .format('MM/DD/YYYY')
+    endDate.value = dayjs()
+      .month(Math.floor(dayjs().month() / 3) * 3 + 2)
+      .endOf('month')
+      .format('MM/DD/YYYY')
+  } else if (val == 'Last Quarter') {
+    startDate.value = dayjs()
+      .month(Math.floor(dayjs().month() / 3) * 3 - 3)
+      .startOf('month')
+      .format('MM/DD/YYYY')
+    endDate.value = dayjs()
+      .month(Math.floor(dayjs().month() / 3) * 3 - 1)
+      .endOf('month')
+      .format('MM/DD/YYYY')
+  } else if (val == 'This Year') {
+    startDate.value = dayjs().startOf('year').format('MM/DD/YYYY')
+    endDate.value = dayjs().endOf('year').format('MM/DD/YYYY')
+  } else if (val == 'Last Year') {
+    startDate.value = dayjs().subtract(1, 'year').startOf('year').format('MM/DD/YYYY')
+    endDate.value = dayjs().subtract(1, 'year').endOf('year').format('MM/DD/YYYY')
+  }
+  emits('changeTimeRange', {
+    startDate: startDate.value,
+    endDate: endDate.value,
+    fieldType: fieldType.value,
+    type: type.value
+  })
+}
+
+const getData = () => {
+  if (!fieldType.value || !type.value) {
+    ElMessage.warning('Please select the Report Data Time Range')
+    return false
+  }
+
+  let startValue: any = ''
+  let endValue: any = ''
+  if (type.value === 'dynamic_rolling') {
+    startValue = clampedValueStart.value
+    endValue = clampedValueEnd.value
+  } else if (type.value === 'fixed') {
+    startValue = startDate.value
+    endValue = endDate.value
+  }
+  return {
+    type: type.value,
+    fieldType: fieldType.value,
+    startValue,
+    endValue
+  }
+}
+
+defineExpose({
+  getData
+})
+</script>
+<template>
+  <div style="padding: 8px 16px 16px 16px">
+    <div class="title">
+      <span class="stars_red">*</span>
+      Data Time Reference Field Selection
+    </div>
+    <el-select
+      class="select_time"
+      v-model="fieldType"
+      @change="changeTime(type)"
+      placeholder="Please select..."
+    >
+      <el-option
+        v-for="item in dataTimeoptions"
+        :key="item.value"
+        :label="item.label"
+        :value="item.value"
+      />
+    </el-select>
+    <div class="schedule_rule">
+      <div class="schedule_title">
+        <span class="stars_red">*</span>
+        Data Range Configuration Method
+      </div>
+      <div class="schedule_container">
+        <el-radio-group v-model="type" @change="changeTime">
+          <el-radio value="dynamic_rolling">
+            <div class="radio_custom">
+              <div style="height: 32px" class="flex">
+                Dynamic Rolling Range
+                <el-tooltip
+                  popper-class="schedule-popper"
+                  effect="dark"
+                  :show-arrow="false"
+                  placement="right"
+                >
+                  <template #content>
+                    Configuration: Past X days to Future Y days, always dynamically calculated based
+                    on current date when generating reports.<br />
+                    Usage: For example, setting 5 days to 3 days means the data range differs each
+                    time it's automatically generated. Used for short-cycle operational monitoring.
+                  </template>
+                  <span class="font_family icon-icon_info_b"></span>
+                </el-tooltip>
+              </div>
+              <div v-if="isShowRolling" class="oceanCheckbox2">
+                <el-radio-group v-model="rollingRangeRadio" @change="ChangeRollingRangeRadio">
+                  <el-radio-button label="Next 30 days" value="Next 30 days" />
+                  <el-radio-button label="Next 60 days" value="Next 60 days" />
+                  <el-radio-button
+                    label="Past 10 days to next 60 days"
+                    value="Past 10 days to next 60 days"
+                  />
+                  <el-radio-button label="Past 30 days" value="Past 30 days" />
+                  <el-radio-button label="Customize" value="Customize" />
+                </el-radio-group>
+                <div
+                  class="flex"
+                  style="align-items: end; margin: 16px 8px 16px 0; flex-wrap: wrap"
+                >
+                  <div class="date_flex" style="margin-right: 10px">
+                    <div class="time_title">Start Date</div>
+                    <div class="flex">
+                      <div class="currentTime">Past</div>
+                      <el-input
+                        v-model="clampedValueStart"
+                        @input="changeTime('dynamic_rolling')"
+                        class="input-with-select"
+                      >
+                      </el-input>
+                      <div class="Days">Day(s)</div>
+                    </div>
+                  </div>
+                  <div class="date_flex">
+                    <div class="time_title">End Date</div>
+                    <div class="flex">
+                      <div class="currentTime">Future</div>
+                      <el-input
+                        v-model="clampedValueEnd"
+                        @input="changeTime('dynamic_rolling')"
+                        class="input-with-select"
+                      >
+                      </el-input>
+                      <div class="Days">Day(s)</div>
+                    </div>
+                  </div>
+                </div>
+              </div>
+            </div>
+          </el-radio>
+          <el-radio value="fixed">
+            <div class="radio_custom">
+              <div style="height: 32px" class="flex">
+                Fixed Range
+                <el-tooltip
+                  :show-arrow="false"
+                  popper-class="schedule-popper"
+                  effect="dark"
+                  placement="right"
+                >
+                  <template #content>
+                    Configuration: Specific start and end dates, always query this range when
+                    automatically generating reports.<br />
+                    Example: Start date [2025-01-01], End date [2025-12-31]
+                  </template>
+                  <span class="font_family icon-icon_info_b"></span>
+                </el-tooltip>
+              </div>
+              <div v-if="isShowFixed">
+                <div class="oceanCheckbox2">
+                  <el-radio-group
+                    v-model="fixedRangeRadio"
+                    @change="changeFixedRange"
+                    class="oceanCheckbox2"
+                  >
+                    <el-radio-button label="This Month" value="This Month" />
+                    <el-radio-button label="Last Month" value="Last Month" />
+                    <el-radio-button label="This Quarter" value="This Quarter" />
+                    <el-radio-button label="Last Quarter" value="Last Quarter" />
+                    <el-radio-button label="This Year" value="This Year" />
+                    <el-radio-button label="Last Year" value="Last Year" />
+                    <el-radio-button label="Customize" value="Customize" />
+                  </el-radio-group>
+                </div>
+                <div style="display: flex; padding-bottom: 16px">
+                  <div style="margin-right: 9px; width: 50%">
+                    <div class="date_text">Start Date</div>
+                    <el-date-picker
+                      v-model="startDate"
+                      type="date"
+                      class="date-picker"
+                      clearable
+                      placeholder="Pick a Date"
+                      :format="userStore.dateFormat"
+                      valueFormat="MM/DD/YYYY"
+                      :disabled-date="disabledDateEnd"
+                      @change="changeTime('fixed')"
+                    />
+                  </div>
+                  <div style="width: 50%">
+                    <div class="date_text">End Date</div>
+                    <el-date-picker
+                      v-model="endDate"
+                      type="date"
+                      class="date-picker"
+                      clearable
+                      placeholder="Pick a Date"
+                      :format="userStore.dateFormat"
+                      valueFormat="MM/DD/YYYY"
+                      :disabled-date="disabledDate"
+                      @change="changeTime('fixed')"
+                    />
+                  </div>
+                </div>
+              </div>
+            </div>
+          </el-radio>
+        </el-radio-group>
+      </div>
+    </div>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.title {
+  color: var(--color-neutral-2);
+  font-size: 12px;
+  font-weight: 400;
+}
+.stars_red {
+  color: var(--color-danger);
+}
+.select_time {
+  margin: 4px 0 8px 0;
+  width: 320px;
+}
+.schedule_rule {
+  border: 1px solid var(--color-border);
+  background-color: var(--color-schedule-bg);
+  border-radius: 12px;
+  .schedule_title {
+    padding: 11px 9px;
+    font-weight: 700;
+    font-size: 14px;
+    color: var(--color-neutral-1);
+  }
+  .schedule_container {
+    margin: 0 9px 8px 9px;
+  }
+}
+:deep(.el-radio-group) {
+  display: block;
+}
+:deep(.el-radio) {
+  display: flex;
+  min-height: 32px;
+  border: 1px solid var(--color-system-border);
+  background-color: var(--color-system-body-bg);
+  margin-bottom: 4px;
+  border-radius: 6px;
+  padding: 0 8px;
+  margin-right: 0;
+  height: fit-content;
+  align-items: start;
+}
+.oceanCheckbox2 {
+  :deep(.el-radio-group) {
+    display: flex;
+    flex-direction: row;
+  }
+}
+.oceanCheckbox2 {
+  :deep(.el-radio-button) {
+    flex: 1;
+    border: 1px solid var(--color-system-input-border);
+    display: flex;
+    align-items: center;
+    justify-content: center;
+  }
+  :deep(.el-radio-button:first-child) {
+    border-radius: 6px 0 0 6px;
+  }
+  :deep(.el-radio-button:last-child) {
+    border-radius: 0 6px 6px 0;
+  }
+}
+:deep(.el-radio-button__inner) {
+  border: none;
+  height: 32px;
+  flex: 1;
+}
+:deep(.el-radio-button:first-child .el-radio-button__inner) {
+  border-left: none;
+  border-radius: 6px 0 0 6px;
+}
+:deep(.el-radio-button:last-child .el-radio-button__inner) {
+  border-radius: 0 6px 6px 0;
+}
+:deep(.radio_custom) {
+  flex-direction: column;
+  width: 100%;
+}
+:deep(.el-radio__input.is-checked + .el-radio__label) {
+  color: var(--color-neutral-1);
+}
+.input-with-select {
+  border-radius: 0;
+}
+:deep(.el-input__wrapper) {
+  border-radius: 0;
+  opacity: 0.8;
+  height: 32px;
+}
+:deep(.el-radio__label) {
+  display: flex;
+  align-items: center;
+  .font_family {
+    color: var(--color-neutral-2);
+    margin-left: 4px;
+  }
+}
+:deep(.el-radio__inner) {
+  margin-top: 6px;
+}
+:deep(.el-radio__input) {
+  margin-top: 2px;
+}
+.date_text {
+  margin-top: 16px;
+  margin-bottom: 4px;
+  line-height: 16px;
+  color: var(--color-neutral-2);
+  font-size: 12px;
+  font-weight: 400;
+}
+.flex {
+  display: flex;
+  align-items: center;
+}
+.currentTime {
+  background-color: var(--el-disabled-bg-color);
+  border-radius: 6px 0 0 6px;
+  padding: 0 16px;
+  height: 32px;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  border: 1px solid var(--color-system-input-border);
+}
+.Days {
+  width: 150px;
+  border: 1px solid var(--color-system-input-border);
+  border-radius: 0 6px 6px 0;
+  background-color: var(--color-system-body-bg);
+  border-left: 0;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  opacity: 0.8;
+  height: 32px;
+}
+.time_title {
+  color: var(--color-neutral-2);
+  font-size: 12px;
+  line-height: 16px;
+  margin-bottom: 4px;
+}
+.date_flex {
+  flex: 1;
+}
+:deep(.date-picker) {
+  width: 100%;
+  .el-input__wrapper {
+    border-radius: 6px;
+  }
+}
+</style>

+ 178 - 0
src/views/Report/src/components/ReportSchedule/src/components/ValidityPeriod.vue

@@ -0,0 +1,178 @@
+<script setup lang="ts">
+import dayjs from 'dayjs'
+import { useUserStore } from '@/stores/modules/user'
+
+const userStore = useUserStore()
+const formatDate = userStore.dateFormat
+
+const props = defineProps<{
+  data: {
+    type: 'custom' | 'permanent' | ''
+    startDate: string
+    endDate: string
+  }
+}>()
+
+const validityPeriodType = ref('')
+const startDate = ref('')
+const endDate = ref('')
+watch(
+  () => props.data,
+  (newVal) => {
+    const isCustom = newVal.type === 'custom'
+    const formatDate = (date: string) =>
+      isCustom && date ? dayjs(date, 'YYYY-MM-DD', true).format('MM/DD/YYYY') : date
+
+    startDate.value = formatDate(newVal.startDate)
+    endDate.value = formatDate(newVal.endDate)
+    validityPeriodType.value = newVal.type
+  },
+  { immediate: true }
+)
+
+const isShowEffective = computed(() => {
+  return validityPeriodType.value === 'custom'
+})
+// 选择Validity Period部分
+const ChangeValidity = () => {
+  startDate.value = ''
+  endDate.value = ''
+}
+
+const disabledDate = (date: any) => {
+  if (!startDate.value) return false
+  const currentDate = dayjs(date).format('YYYY-MM-DD')
+  return dayjs(currentDate).isBefore(startDate.value)
+}
+
+const disabledDateEnd = (date: any) => {
+  if (!endDate.value) return false
+  const currentDate = dayjs(date).format('YYYY-MM-DD')
+  return dayjs(currentDate).isAfter(endDate.value)
+}
+const getData = () => {
+  if (
+    !validityPeriodType.value ||
+    (validityPeriodType.value === 'custom' && (!startDate.value || !endDate.value))
+  ) {
+    ElMessage.warning('Please select the Schedule Rule Validity Period')
+    return false
+  }
+  return {
+    type: validityPeriodType.value,
+    startDate: startDate.value ? startDate.value : '',
+    endDate: endDate.value ? endDate.value : ''
+  }
+}
+
+defineExpose({
+  getData
+})
+</script>
+<template>
+  <div style="padding: 8px 16px">
+    <el-radio-group v-model="validityPeriodType" @change="ChangeValidity">
+      <el-radio value="permanent">
+        Permanent Valid
+        <el-tooltip
+          popper-class="schedule-popper"
+          effect="dark"
+          :show-arrow="false"
+          content="Active continuously once enabled, until manually disabled or deleted."
+          placement="right"
+        >
+          <span class="font_family icon-icon_info_b"></span>
+        </el-tooltip>
+      </el-radio>
+      <el-radio value="custom">
+        <div class="radio_custom">
+          <div style="height: 32px" class="flex">
+            Custom Period
+            <el-tooltip
+              popper-class="schedule-popper"
+              effect="dark"
+              :show-arrow="false"
+              content="Only automatically execute during specified time period."
+              placement="right"
+            >
+              <span class="font_family icon-icon_info_b"></span>
+            </el-tooltip>
+          </div>
+          <div v-if="isShowEffective" style="display: flex; margin-bottom: 8px">
+            <div style="margin-right: 9px">
+              <div class="date_text">Effective Start Date</div>
+              <el-date-picker
+                v-model="startDate"
+                type="date"
+                style="width: 320px"
+                placeholder="Pick a Date"
+                clearable
+                :format="formatDate"
+                :disabled-date="disabledDateEnd"
+                valueFormat="MM/DD/YYYY"
+              />
+            </div>
+            <div>
+              <div class="date_text">Effective End Date</div>
+              <el-date-picker
+                v-model="endDate"
+                type="date"
+                style="width: 320px"
+                placeholder="Pick a Date"
+                clearable
+                :format="formatDate"
+                :disabled-date="disabledDate"
+                valueFormat="MM/DD/YYYY"
+              />
+            </div>
+          </div>
+        </div>
+      </el-radio>
+    </el-radio-group>
+  </div>
+</template>
+<style lang="scss" scoped>
+.flex {
+  display: flex;
+  align-items: center;
+}
+:deep(.el-radio-group) {
+  display: block;
+}
+:deep(.el-radio) {
+  display: flex;
+  min-height: 32px;
+  border: 1px solid var(--color-system-border);
+  background-color: var(--color-system-body-bg);
+  margin-bottom: 8px;
+  border-radius: 6px;
+  padding: 0 8px;
+  margin-right: 0;
+  height: fit-content;
+  line-height: 32px;
+  align-items: start;
+}
+:deep(.radio_custom) {
+  flex-direction: column;
+}
+:deep(.el-radio__input.is-checked + .el-radio__label) {
+  color: var(--color-neutral-1);
+}
+:deep(.el-radio__label) {
+  display: flex;
+  align-items: center;
+  .font_family {
+    color: var(--color-neutral-2);
+    margin-left: 4px;
+  }
+}
+:deep(.el-radio__inner) {
+  margin-top: 7px;
+}
+.date_text {
+  height: 26px;
+  color: var(--color-neutral-2);
+  font-size: 12px;
+  font-weight: 400;
+}
+</style>

BIN
src/views/Report/src/images/empty-dark.png


BIN
src/views/Report/src/images/empty-light.png


+ 1 - 0
src/views/TemplateManagement/index.ts

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

+ 186 - 0
src/views/TemplateManagement/src/TemplateManagement.vue

@@ -0,0 +1,186 @@
+<script lang="ts" setup>
+import { useCalculatingHeight } from '@/hooks/calculatingHeight'
+import TableView from './components/TableView'
+import { useRouter } from 'vue-router'
+
+const router = useRouter()
+const filterRef: Ref<HTMLElement | null> = ref(null)
+const containerHeight = useCalculatingHeight(document.documentElement, 250, [filterRef])
+const queryData = ref({
+  text_search: '',
+  is_active: '',
+  application_scope: '',
+  party_id: ''
+})
+
+const aiModelList = ref([])
+
+const activeOptions = [
+  {
+    label: 'Active',
+    value: true
+  },
+  {
+    label: 'Inactive',
+    value: false
+  }
+]
+
+const applicationScopeOptions = [
+  {
+    label: 'All Users',
+    value: 'all'
+  },
+  {
+    label: 'Specific Users',
+    value: 'specific'
+  }
+]
+
+const tableRef = ref()
+
+onMounted(() => {
+  $api.getFilterPartyID().then((res) => {
+    if (res.code === 200) {
+      aiModelList.value = res.data
+    }
+  })
+})
+
+const Search = () => {
+  tableRef.value.searchTableData(queryData.value)
+}
+
+const handleCreate = () => {
+  // Navigate to the Create Report Template page
+  router.push({
+    name: 'Create Report Template'
+  })
+}
+</script>
+<template>
+  <div class="dashboard">
+    <div class="Title">
+      <span>Report Template Management</span>
+      <el-button class="el-button--main" @click="handleCreate">
+        <span class="font_family icon-icon_add_b"></span> Create New Report Template</el-button
+      >
+    </div>
+    <div class="display">
+      <div class="heaer_top">
+        <div class="input-tips_filter">
+          <el-input
+            placeholder="Search report name"
+            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>
+
+        <div class="tips_filter">
+          <el-select v-model="queryData.is_active" clearable placeholder="Is Active">
+            <el-option
+              v-for="item in activeOptions"
+              :key="item.label"
+              :label="item.label"
+              :value="item.value"
+            />
+          </el-select>
+        </div>
+        <div class="tips_filter">
+          <el-select
+            v-model="queryData.application_scope"
+            clearable
+            placeholder="Application Scope"
+          >
+            <el-option
+              v-for="item in applicationScopeOptions"
+              :key="item.label"
+              :label="item.label"
+              :value="item.value"
+            />
+          </el-select>
+        </div>
+        <div class="tips_filter">
+          <el-select v-model="queryData.party_id" clearable placeholder="Party ID">
+            <el-option
+              v-for="item in aiModelList"
+              :key="item.value"
+              :label="item.label"
+              :value="item.value"
+            />
+          </el-select>
+        </div>
+
+        <el-button class="el-button--dark" @click="Search">Search</el-button>
+      </div>
+    </div>
+    <TableView :height="containerHeight" ref="tableRef"></TableView>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.Title {
+  display: flex;
+  justify-content: space-between;
+  height: 68px;
+  border-bottom: 1px solid var(--color-border);
+  font-size: var(--font-size-6);
+  font-weight: 700;
+  padding: 0 24px;
+  align-items: center;
+}
+.heaer_top {
+  margin-top: 6.57px;
+  margin-bottom: 8px;
+  padding-right: 8px;
+  display: flex;
+}
+
+.display {
+  border: 1px solid var(--color-border);
+  border-width: 0 0 1px 0;
+  padding-left: 23.52px;
+}
+
+.tips_filter {
+  flex: 1;
+  height: 30px;
+  max-width: 190px;
+  margin-right: 8px;
+}
+.input-tips_filter {
+  flex: 1;
+  max-width: 320px;
+  height: 32px;
+  margin-right: 8px;
+  :deep(.el-input__wrapper) {
+    height: 32px;
+  }
+}
+.date-tips_filter {
+  flex: 1;
+  max-width: 250px;
+  height: 32px;
+  margin-right: 8px;
+}
+.comparator-tips_filter {
+  flex: 1;
+  display: flex;
+  align-items: center;
+  max-width: 260px;
+  height: 32px;
+  margin-right: 8px;
+}
+.dashboard {
+  position: relative;
+  background-color: var(--color-mode);
+}
+</style>

+ 1 - 0
src/views/TemplateManagement/src/components/CreateReportTemplate/index.ts

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

+ 870 - 0
src/views/TemplateManagement/src/components/CreateReportTemplate/src/CreateReportTemplate.vue

@@ -0,0 +1,870 @@
+<script lang="ts" setup>
+import { useRouter, useRoute } from 'vue-router'
+import partyIDSelect from './components/partyIDSelect.vue'
+import GroupNameSelect from './components/GroupNameSelect.vue'
+import AccountSelect from './components/AccountSelect.vue'
+import { VueDraggable } from 'vue-draggable-plus'
+import AdjustmentField from './components/AdjustmentField.vue'
+import { cloneDeep } from 'lodash'
+
+const router = useRouter()
+const route = useRoute()
+
+const infoData = ref({
+  reportName: '',
+  reportLevel: '',
+  reportDescription: ''
+})
+const pageLoading = ref(false)
+onMounted(() => {
+  if (route.query.serial_no) {
+    pageLoading.value = true
+    $api
+      .editReportTemplate({ serial_no: route.query.serial_no })
+      .then((res) => {
+        if (res.code !== 200) return
+        const data = res.data
+        infoData.value = {
+          // 如果是复制的话,清空Report Name
+          reportName: route.query.copy !== 't' ? data.reportName : '',
+          reportLevel: data.reportLevel,
+          reportDescription: data.reportDescription
+        }
+        fieldsList.value = data.reportFields.map((item) => {
+          return {
+            ...item,
+            uniqueId: generate8DigitUnique()
+          }
+        })
+        const reportAccess = data.reportAccess
+        accessControlType.value = reportAccess.type
+        specificRoles.value = {
+          partyId: reportAccess.partyId,
+          groupName: reportAccess.groupName,
+          systemAccount: reportAccess.systemAccount
+        }
+      })
+      .finally(() => {
+        pageLoading.value = false
+        watch(accessControlType, (newVal) => {
+          if (newVal === 'Specific Roles') {
+            // 等待下一个渲染周期结束后,获取detailRef的高度
+            nextTick(() => {
+              if (detailRef.value) {
+                detailRef.value.scrollIntoView({
+                  behavior: 'smooth', // 平滑滚动
+                  block: 'start' // 滚动到顶部对齐
+                })
+              }
+            })
+          }
+        })
+      })
+  }
+})
+
+interface Field {
+  uniqueId: string
+  field: string
+  title: string
+  displayName: string
+  fieldType: string
+  value?: string
+  isFilter: boolean
+  isSort: boolean
+  groupName: string
+}
+const fieldsList = ref<Field[]>([])
+const levelOptions = [
+  {
+    label: 'Shipment level',
+    value: 'Shipment level'
+  },
+  {
+    label: 'Container Level',
+    value: 'Container Level'
+  },
+  {
+    label: 'Item Level',
+    value: 'Item Level'
+  }
+]
+
+// 前端使用的唯一标识符
+const generate8DigitUnique = () => {
+  return Math.floor(10000000 + Math.random() * 90000000).toString()
+}
+
+const changeFieldConfig = (state: any, field: string, index: number, key: string) => {
+  if (!state) return
+  fieldsList.value.forEach((item) => {
+    if (item.field === field) {
+      item[key] = false
+    }
+  })
+  const firstMatch = fieldsList.value[index]
+  if (firstMatch) {
+    firstMatch[key] = true
+  }
+}
+
+const handleDeleteField = (index: number, uniqueId: string) => {
+  fieldsList.value.splice(index, 1)
+
+  Object.keys(copyFieldsList.value).forEach((key) => {
+    copyFieldsList.value[key] = copyFieldsList.value[key].filter(
+      (item) => item.uniqueId !== uniqueId
+    )
+  })
+}
+const copyFieldsList = ref({})
+const handleCopyField = (index: number) => {
+  const curField = cloneDeep(fieldsList.value[index])
+  curField.displayName = `${curField.displayName} (Copy)`
+  curField.isFilter = false
+  curField.isSort = false
+  curField.uniqueId = generate8DigitUnique()
+  if (copyFieldsList.value[curField.field]) {
+    copyFieldsList.value[curField.field].push(cloneDeep(curField))
+  } else {
+    copyFieldsList.value[curField.field] = [cloneDeep(curField)]
+  }
+  fieldsList.value.splice(index + 1, 0, cloneDeep(curField))
+}
+
+const AdjustmentFieldRef = ref()
+// 打开定制表格弹窗
+const handleCustomizeColumns = () => {
+  if (!infoData.value.reportLevel) {
+    ElMessage.warning('Please select the report level.')
+    return
+  }
+  const params = {
+    serial_no: '',
+    level: infoData.value.reportLevel
+  }
+  const seen = new Set()
+  const uniqueArray = fieldsList.value.filter((item) => {
+    if (seen.has(item.field)) {
+      return false // 已存在,跳过
+    }
+    seen.add(item.field)
+    return true // 第一次出现,保留
+  })
+
+  AdjustmentFieldRef.value.openDialog(
+    params,
+    -220,
+    'Drag item over to this selection or click "add" icon to show the field on report template list',
+    uniqueArray
+  )
+}
+
+const newFieldInfo = ref<{
+  name: string
+  fieldType: 'Blank' | 'Fixed Value'
+  value: string
+}>({
+  name: '',
+  fieldType: 'Blank',
+  value: ''
+})
+const addNewFieldVisible = ref(false)
+// 添加新字段
+const handleAddNewField = () => {
+  addNewFieldVisible.value = true
+}
+const handleFieldTypeChange = () => {
+  newFieldInfo.value.value = ''
+}
+const addNewField = () => {
+  if (!newFieldInfo.value.name?.trim()) {
+    ElMessage.warning('Please enter the new field name.')
+    return
+  }
+
+  if (newFieldInfo.value.fieldType === 'Fixed Value' && !newFieldInfo.value.value?.trim()) {
+    ElMessage.warning('Please enter the fixed value.')
+    return
+  }
+  fieldsList.value.unshift({
+    uniqueId: generate8DigitUnique(),
+    field: newFieldInfo.value.name,
+    title: newFieldInfo.value.name,
+    displayName: newFieldInfo.value.name,
+    value: newFieldInfo.value.value,
+    fieldType: 'Custom',
+    groupName: '',
+    isFilter: false,
+    isSort: false
+  })
+  newFieldInfo.value = {
+    name: '',
+    fieldType: 'Blank',
+    value: ''
+  }
+  addNewFieldVisible.value = false
+}
+// 调整应用字段
+const handleApplay = (data: any) => {
+  const customizeData = fieldsList.value.filter((item: any) => item.field_type === 'Custom')
+  fieldsList.value = data.map((item: any) => {
+    return {
+      ...item,
+      label: item.label,
+      title: item.title || item.label,
+      displayName: item.displayName || item.label,
+      isFilter: !!item.isFilter,
+      isSort: !!item.isSort,
+      uniqueId: item.uniqueId || generate8DigitUnique()
+    }
+  })
+  fieldsList.value = [...customizeData, ...fieldsList.value]
+
+  const validFields = new Set(fieldsList.value.map((item) => item.field))
+  for (const field in copyFieldsList.value) {
+    if (!validFields.has(field)) {
+      delete copyFieldsList.value[field]
+    }
+  }
+
+  // === 第三步:从后往前插入副本(不修改原始项)===
+  for (let i = fieldsList.value.length - 1; i >= 0; i--) {
+    const field = fieldsList.value[i].field
+    const copies = copyFieldsList.value[field]
+
+    if (copies?.length) {
+      // 插入副本到原项后面
+      fieldsList.value.splice(i + 1, 0, ...copies)
+    }
+  }
+}
+
+const accessControlType = ref('All Users')
+const detailRef: Ref<HTMLElement | null> = ref(null)
+
+const specificRoles = ref({
+  partyId: [],
+  groupName: [],
+  systemAccount: []
+})
+const changePartyId = (val: string[]) => {
+  specificRoles.value.partyId = val
+}
+const changeGroupName = (val: string[]) => {
+  specificRoles.value.groupName = val
+}
+
+const changeAccount = (val: string[]) => {
+  specificRoles.value.systemAccount = val
+}
+
+const fieldLoading = ref(false)
+const handleRightRemove = () => {}
+
+const handleCancel = () => {
+  router.push('/template-management')
+}
+
+const handlePageSave = () => {
+  let verified = true
+  if (!infoData.value.reportName.trim()) {
+    ElMessage.warning('Please enter the Report Name.')
+    verified = false
+  }
+  if (!infoData.value.reportLevel) {
+    ElMessage.warning('Please enter the Report Level')
+    verified = false
+  }
+  if (!infoData.value.reportDescription.trim()) {
+    ElMessage.warning('Please enter the Report Description.')
+    verified = false
+  }
+  if (
+    accessControlType.value === 'Specific Roles' &&
+    specificRoles.value.partyId?.length === 0 &&
+    specificRoles.value.groupName?.length === 0 &&
+    specificRoles.value.systemAccount?.length === 0
+  ) {
+    ElMessage.warning('Please select Party ID or Group Name for Specific Roles access control.')
+    verified = false
+  }
+  if (!verified) {
+    return
+  }
+  pageLoading.value = true
+  const data = {
+    report_name: infoData.value.reportName,
+    report_level: infoData.value.reportLevel,
+    report_description: infoData.value.reportDescription,
+    access_type: accessControlType.value,
+    party_ids: specificRoles.value.partyId || [],
+    group_names: specificRoles.value.groupName || [],
+    system_account: specificRoles.value.systemAccount || [],
+    fieldsList: fieldsList.value
+  }
+  let serial_no = ''
+  if (route.query.copy !== 't' && route.query.serial_no) {
+    serial_no = String(route.query.serial_no)
+  }
+  $api
+    .saveNewReportTemplate({ ...data, serial_no })
+    .then((res: any) => {
+      if (res.code === 200) {
+        ElMessage.success('Report Template saved successfully!')
+        router.push('/template-management')
+      } else {
+        ElMessage.error(res.data.msg || 'Failed to save Report Template.')
+      }
+    })
+    .finally(() => {
+      pageLoading.value = false
+    })
+}
+</script>
+<template>
+  <div class="dashboard" v-vloading="pageLoading">
+    <div class="Title">
+      <span>Create New Report Template</span>
+      <div class="button-group">
+        <el-button type="default" @click="handleCancel">
+          <span class="font_family icon-icon_return_b" style="margin-right: 3px"></span
+          >Cancel</el-button
+        >
+        <el-button class="el-button--main" @click="handlePageSave">
+          <span class="font_family icon-icon_save_b" style="margin-right: 3px"></span
+          >Save</el-button
+        >
+      </div>
+    </div>
+    <div class="display">
+      <div class="basic-info template-box">
+        <div class="header">Basic Report Information</div>
+        <div class="content-box">
+          <div class="info-item" style="display: flex; gap: 8px">
+            <div class="report-name" style="flex: 1">
+              <div class="label">
+                <span style="color: var(--color-danger)">*</span>
+                <span>Report Name</span>
+              </div>
+              <el-input v-model="infoData.reportName" placeholder="Please enter..."></el-input>
+            </div>
+            <div class="report-level" style="flex: 1">
+              <div class="label">
+                <span style="color: var(--color-danger)">*</span>
+                <span>Report Level</span>
+              </div>
+              <el-select v-model="infoData.reportLevel" placeholder="Please enter...">
+                <el-option
+                  v-for="item in levelOptions"
+                  :label="item.label"
+                  :value="item.value"
+                  :key="item.value"
+                ></el-option>
+              </el-select>
+            </div>
+          </div>
+          <div class="info-item">
+            <div class="label">
+              <span style="color: var(--color-danger)">*</span>
+              <span>Report Description</span>
+            </div>
+            <el-input
+              type="textarea"
+              v-model="infoData.reportDescription"
+              placeholder="Please enter..."
+            ></el-input>
+          </div>
+        </div>
+      </div>
+      <div class="fields-configuration template-box">
+        <div class="header">
+          <span>Report Fields Configuration</span>
+
+          <div class="right-option">
+            <el-button
+              class="el-button--dark"
+              @click="handleAddNewField"
+              style="width: 148px; padding-top: 11px"
+            >
+              <span style="margin-right: 3px" class="font_family icon-icon_add_b"></span>
+              <span>Add New Field</span>
+            </el-button>
+            <el-button
+              v-if="fieldsList.length > 0"
+              class="el-button--dark"
+              @click="handleCustomizeColumns()"
+              style="width: 110px; padding-top: 11px"
+            >
+              <span style="margin-right: 3px" class="font_family icon-icon_add_b"></span>
+              <span>Select Field</span>
+            </el-button>
+          </div>
+        </div>
+        <div class="content-box">
+          <div class="empty-box" v-if="fieldsList.length === 0">
+            <el-button class="el-button--dark" @click="handleCustomizeColumns">
+              <span class="font_family icon-icon_add_b"></span>Add/Edit Field
+            </el-button>
+            <p>No field selected. click “Add Field” to get started.</p>
+          </div>
+          <div class="fields-list" v-else>
+            <VueDraggable
+              v-vloading="fieldLoading"
+              v-model="fieldsList"
+              class="column-list"
+              ghost-class="ghost-column"
+              :forceFallback="true"
+              fallback-class="fallback-class"
+              group="customizeColumns"
+              item-key="uniqueId"
+              @end="handleRightRemove"
+              handle=".handle-draggable"
+            >
+              <div
+                class="field-item"
+                v-for="(fieldItem, index) in fieldsList"
+                :key="fieldItem.uniqueId"
+              >
+                <span
+                  class="font_family icon-icon_dragsort__b draggable-icon handle-draggable"
+                  style="margin-right: 12px; font-size: 16px"
+                ></span>
+                <div class="label handle-draggable">
+                  <span style="font-weight: 700">[{{ fieldItem.field }}]</span>
+                  <span style="margin-left: 8px">{{ fieldItem.title }}</span>
+                </div>
+                <el-input
+                  :id="fieldItem.uniqueId"
+                  :name="fieldItem.uniqueId"
+                  class="display-name"
+                  v-model="fieldItem.displayName"
+                  placeholder="Display Name in Report"
+                ></el-input>
+                <div class="actions">
+                  <div class="checkbox-group">
+                    <el-checkbox
+                      :disabled="
+                        fieldItem.fieldType !== 'System' ||
+                        fieldItem.groupName === 'Container Status' ||
+                        fieldItem.groupName === 'Milestone'
+                      "
+                      v-model="fieldItem.isFilter"
+                      @change="changeFieldConfig($event, fieldItem.field, index, 'isFilter')"
+                      >Filter</el-checkbox
+                    >
+                    <el-checkbox
+                      :disabled="fieldItem.fieldType !== 'System'"
+                      v-model="fieldItem.isSort"
+                      @change="changeFieldConfig($event, fieldItem.field, index, 'isSort')"
+                      >Sort</el-checkbox
+                    >
+                  </div>
+                  <span
+                    style="margin-right: 4px"
+                    @click="handleCopyField(index)"
+                    class="font_family icon-icon_clone_b"
+                  ></span>
+                  <span
+                    @click="handleDeleteField(index, fieldItem.uniqueId)"
+                    class="font_family icon-icon_delete_b"
+                  ></span>
+                </div>
+              </div>
+            </VueDraggable>
+          </div>
+        </div>
+      </div>
+      <div class="report-access-control template-box">
+        <div class="header">Report Access Control</div>
+        <div class="content-box">
+          <el-radio-group class="radio-group" v-model="accessControlType">
+            <el-radio class="radio-item" value="All Users">
+              <template #default>
+                <div class="radio-content">
+                  <p class="label">All Users</p>
+                  <p class="description">
+                    This report will be available to all users in the system
+                  </p>
+                </div>
+              </template>
+            </el-radio>
+            <el-radio class="radio-item specific-roles" value="Specific Roles">
+              <template #default>
+                <div class="radio-content">
+                  <div class="top-options">
+                    <p class="label">Specific Roles</p>
+                    <p class="description">Restrict access to specific user roles</p>
+                  </div>
+                  <div
+                    class="extended-filter"
+                    v-show="accessControlType === 'Specific Roles'"
+                    ref="detailRef"
+                  >
+                    <div class="dividing-line"></div>
+                    <div class="filter-item" style="margin-bottom: 16px">
+                      <div class="label">
+                        <span style="color: var(--color-danger)">*</span>
+                        <span>Party ID</span>
+                      </div>
+                      <partyIDSelect
+                        @change-data="changePartyId"
+                        :data="specificRoles.partyId"
+                      ></partyIDSelect>
+                    </div>
+                    <div class="filter-item" style="margin-bottom: 16px">
+                      <div class="label">
+                        <span style="color: var(--color-danger)">*</span>
+                        <span>Group Name</span>
+                      </div>
+                      <GroupNameSelect
+                        @change-data="changeGroupName"
+                        :data="specificRoles.groupName"
+                      ></GroupNameSelect>
+                    </div>
+                    <div class="filter-item">
+                      <div class="label">
+                        <span style="color: var(--color-danger)">*</span>
+                        <span>KLN ONLINE Account</span>
+                      </div>
+                      <AccountSelect
+                        @change-data="changeAccount"
+                        :data="specificRoles.systemAccount"
+                      ></AccountSelect>
+                    </div>
+                  </div>
+                </div>
+              </template>
+            </el-radio>
+          </el-radio-group>
+        </div>
+      </div>
+    </div>
+
+    <AdjustmentField @apply="handleApplay" ref="AdjustmentFieldRef" />
+    <el-dialog
+      class="add-new-field-dialog"
+      title="Add New Field"
+      v-model="addNewFieldVisible"
+      width="480"
+    >
+      <div>
+        <div class="field-item">
+          <div class="label">
+            <span class="required-symbol">*</span>
+            <span>New Field Name</span>
+          </div>
+          <el-input placeholder="Please enter..." v-model="newFieldInfo.name"></el-input>
+        </div>
+        <div class="field-item field-value">
+          <div class="label">
+            <span class="required-symbol">*</span>
+            <span>Field Value</span>
+          </div>
+          <el-radio-group v-model="newFieldInfo.fieldType" @change="handleFieldTypeChange">
+            <el-radio label="Blank">Blank</el-radio>
+            <el-radio label="Fixed Value">Fixed Value</el-radio>
+          </el-radio-group>
+        </div>
+        <div class="field-item" v-if="newFieldInfo.fieldType === 'Fixed Value'">
+          <div class="label">
+            <span class="required-symbol">*</span>
+            <span>Fixed Value</span>
+          </div>
+          <el-input placeholder="Please enter..." v-model="newFieldInfo.value"></el-input>
+        </div>
+      </div>
+      <template #footer>
+        <el-button
+          style="height: 40px; width: 115px"
+          class="cancel-btn"
+          type="default"
+          @click="addNewFieldVisible = false"
+          >Cancel</el-button
+        >
+        <el-button style="height: 40px; width: 120px" class="el-button--dark" @click="addNewField"
+          >Apply</el-button
+        >
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.Title {
+  position: sticky;
+  top: 0;
+  z-index: 100;
+  display: flex;
+  justify-content: space-between;
+  height: 68px;
+  border-bottom: 1px solid var(--color-border);
+  font-size: var(--font-size-6);
+  font-weight: 700;
+  padding: 0 24px;
+  align-items: center;
+  background-color: var(--color-mode);
+}
+.heaer_top {
+  margin-top: 6.57px;
+  margin-bottom: 8px;
+  padding-right: 8px;
+  display: flex;
+}
+
+.display {
+  max-height: calc(100vh - 140px);
+  border: 1px solid var(--color-border);
+  border-bottom: none;
+  border-width: 0 0 1px 0;
+  padding: 16px 24px 12px;
+  overflow: auto;
+}
+.template-box {
+  margin-bottom: 8px;
+  border: 1px solid var(--color-border);
+  border-radius: 12px;
+  overflow: hidden;
+  .header {
+    height: 48px;
+    border-bottom: 1px solid var(--color-border);
+    display: flex;
+    align-items: center;
+    padding: 0 16px;
+    border-radius: 12px 12px 0 0;
+    background-color: var(--color-header-bg);
+    font-size: 18px;
+    font-weight: bold;
+  }
+  .right-option {
+    margin-left: auto;
+  }
+  .content-box {
+    height: 100%;
+    padding: 8px 16px 16px;
+  }
+}
+.fields-configuration {
+  div.content-box {
+    display: flex;
+    align-items: flex-start;
+    justify-content: center;
+    min-height: 272px;
+    max-height: 400px;
+    width: 100%;
+    padding-bottom: 8px;
+    padding-right: 0px;
+    // overflow: auto;
+    .empty-box {
+      align-self: center;
+      width: 100%;
+      text-align: center;
+      p {
+        margin-top: 12px;
+        color: var(--color-neutral-2);
+      }
+    }
+    .fields-list {
+      width: 100%;
+      max-height: 400px;
+      padding: 8px 0;
+      padding-right: 16px;
+      overflow: auto;
+      user-select: none;
+      .field-item {
+        display: flex;
+        align-items: center;
+        height: 48px;
+        margin-bottom: 8px;
+        padding: 0 16px;
+        border-radius: 6px;
+        border: 1px solid var(--color-border);
+        .label {
+          flex: 1;
+          .required-symbol {
+            color: var(--color-danger);
+          }
+        }
+        .display-name {
+          flex: 1.2;
+          margin: 0 16px;
+          :deep(.el-input__wrapper) {
+            height: 32px;
+          }
+        }
+        .actions {
+          display: flex;
+          align-items: center;
+          justify-content: space-between;
+          width: 240px;
+          padding-left: 30px;
+          .checkbox-group {
+            display: flex;
+          }
+          .el-checkbox {
+            margin-right: 16px;
+            :deep(.el-checkbox__inner) {
+              height: 16px;
+              width: 16px;
+              &::after {
+                border-width: 2px;
+                height: 9px;
+                width: 5px;
+                left: 4px;
+              }
+            }
+            :deep(.el-checkbox__label) {
+              margin-top: 3px;
+              padding-left: 4px;
+              line-height: 2;
+            }
+          }
+          .font_family {
+            float: right;
+            cursor: pointer;
+          }
+        }
+      }
+    }
+  }
+  .ghost-column {
+    cursor: move !important;
+    span {
+      opacity: 0;
+    }
+    border: 1px dashed var(--color-customize-column-item-drag-border) !important;
+    background-color: var(--color-customize-column-item-drag-bg) !important;
+    box-shadow: none !important;
+  }
+
+  .fallback-class {
+    opacity: 1 !important;
+    background-color: var(--color-customize-column-item-hover-bg) !important;
+    cursor: move !important;
+  }
+}
+.basic-info {
+  .info-item {
+    &:first-child {
+      margin-bottom: 16px;
+    }
+    .label {
+      margin-bottom: 4px;
+      font-size: 12px;
+      span {
+        color: var(--color-neutral-2);
+      }
+    }
+  }
+}
+
+.report-access-control {
+  .content-box {
+    .radio-group {
+      display: flex;
+      align-items: center;
+      flex-direction: column;
+      gap: 8px;
+    }
+    .radio-item {
+      align-items: flex-start;
+      width: 100%;
+      // min-height: 80px;
+      height: auto;
+      margin-right: 24px;
+      margin-top: 4px;
+      padding: 20px 16px;
+      font-size: 14px;
+      border: 1px solid var(--color-border);
+      border-radius: 12px;
+      .radio-content {
+        height: auto;
+      }
+      // .top-options {
+      .label {
+        font-weight: 700;
+      }
+      .description {
+        margin-top: 8px;
+        font-size: 12px;
+        color: var(--color-neutral-2);
+      }
+      // }
+    }
+    .specific-roles {
+      position: relative;
+      .dividing-line {
+        position: absolute;
+        left: 0px;
+        top: 66px;
+        margin: 12px 0;
+        height: 1px;
+        width: 100%;
+        background-color: var(--color-border);
+      }
+    }
+    .extended-filter {
+      margin-top: 28px;
+      border-radius: 6px;
+
+      .filter-item {
+        .label {
+          margin-bottom: 4px;
+          span {
+            font-size: 12px;
+            color: var(--color-neutral-2);
+          }
+        }
+      }
+    }
+    // .radio-item.specific-roles {
+    //   padding: 0;
+    // }
+  }
+}
+
+.dashboard {
+  position: relative;
+  background-color: var(--color-mode);
+  .button-group {
+    .el-button {
+      height: 40px;
+      padding: 8px 32px;
+    }
+  }
+}
+</style>
+<style lang="scss">
+.add-new-field-dialog {
+  .field-item {
+    margin-bottom: 16px;
+    .label {
+      margin-bottom: 4px;
+    }
+    &:last-child {
+      margin-bottom: 0;
+    }
+  }
+  .field-value {
+    .el-radio-group {
+      width: 100%;
+      .el-radio {
+        flex: 1;
+        margin-right: 0;
+        padding-left: 12px;
+        border: 1px solid var(--color-border);
+        &:first-child {
+          border-radius: 6px 0 0 6px;
+          border-right: none;
+        }
+        &:last-child {
+          border-radius: 0 6px 6px 0;
+        }
+        .el-radio__label {
+          color: var(--color-neutral-1);
+        }
+      }
+    }
+  }
+}
+</style>

+ 143 - 0
src/views/TemplateManagement/src/components/CreateReportTemplate/src/components/AccountSelect.vue

@@ -0,0 +1,143 @@
+<script setup lang="ts">
+import { cloneDeep, debounce } from 'lodash'
+import axios from 'axios'
+
+const props = defineProps({
+  data: {
+    type: Array as () => string[],
+    default: () => []
+  }
+})
+
+const selectData = ref<string[]>([])
+watch(
+  () => props.data,
+  (newValue) => {
+    selectData.value = cloneDeep(newValue) || []
+  },
+  {
+    deep: true,
+    immediate: true
+  }
+)
+const emit = defineEmits(['changeData'])
+const changeData = (val: string[]) => {
+  // 同步选中状态
+  emit('changeData', val)
+}
+
+interface ListItem {
+  label: string
+  id: string
+}
+
+const options = ref<ListItem[]>([])
+const loading = ref(false)
+const currentController = ref<AbortController | null>(null)
+
+const remoteMethod = (query: string) => {
+  currentController.value?.abort()
+
+  const newController = new AbortController()
+  currentController.value = newController
+  loading.value = true
+
+  $api
+    .getSpecificRolesAccount({ 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()
+})
+
+// 首次聚焦或输入时加载(可选:如果希望空搜也加载)
+// 但通常 remote 场景是“输入才搜”,所以这里只在 filter 时调用
+</script>
+
+<template>
+  <el-select
+    :model-value="selectData"
+    multiple
+    filterable
+    reserve-keyword
+    placeholder="Select Party IDs (Multi-select allowed)"
+    :loading="loading"
+    style="width: 100%"
+    popper-class="part-id-select-popper"
+    :filter-method="debouncedRemoteMethod"
+    @change="changeData"
+  >
+    <el-option v-for="item in options" :key="item.id" :label="item.label" :value="item.label">
+      <div class="select-option">
+        <el-checkbox :model-value="selectData.includes(item.label)" style="flex: 1">
+          <span style="display: inline-block; width: 220px">{{ item.label }}</span>
+        </el-checkbox>
+      </div>
+    </el-option>
+  </el-select>
+</template>
+
+<style lang="scss" scoped>
+.select-option {
+  display: flex;
+  align-items: center;
+  gap: 16px;
+  width: 100%;
+  & > span {
+    margin-left: 56px;
+    line-height: 16px;
+  }
+}
+
+:deep(.el-checkbox__inner) {
+  height: 16px;
+  width: 16px;
+  &::after {
+    border-width: 2px;
+    height: 9px;
+    width: 5px;
+    left: 4px;
+    top: 0;
+  }
+}
+:deep(.el-checkbox__label) {
+  flex: 1;
+  display: flex;
+}
+.text-ellipsis {
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  overflow: hidden;
+}
+</style>
+
+<style lang="scss">
+.part-id-select-popper {
+  // width: 100% !important;
+  // width: auto;
+  // min-width: unset !important;
+}
+</style>

+ 792 - 0
src/views/TemplateManagement/src/components/CreateReportTemplate/src/components/AdjustmentField.vue

@@ -0,0 +1,792 @@
+<script setup lang="ts">
+import { VueDraggable } from 'vue-draggable-plus'
+import { useRoute } from 'vue-router'
+
+const route = useRoute()
+const dialogVisible = ref(false)
+// search筛选的字段
+const searchColumn = ref('')
+// search筛选的options
+const searchOptions: any = ref()
+const dragTips = ref()
+// 右侧箭头消失所需要translateX的值
+const rightArrowHideDistance = ref(0)
+// 控制tab栏的左右切换箭头
+const handleTabArrow = () => {
+  const parentElement: HTMLElement | null = document.querySelector('.left-all-columns')
+  if (!parentElement) return
+
+  // 左侧切换箭头
+  const leftArrow: HTMLElement | null = parentElement.querySelector('.el-tabs__nav-prev')
+  // 右侧切换箭头
+  const rightArrow: HTMLElement | null = parentElement.querySelector('.el-tabs__nav-next')
+  const targetObserverElement = parentElement.querySelector('.el-tabs__nav')
+
+  if (!targetObserverElement || !leftArrow || !rightArrow) return
+
+  // 创建一个函数来获取 translateX
+  const getTranslateX = () => {
+    const style = window.getComputedStyle(targetObserverElement)
+    const matrix = style.transform
+
+    if (matrix !== 'none' && matrix) {
+      // 提取 matrix 中的 translateX 值
+      const values = matrix.match(/matrix\(([^)]+)\)/)?.[1].split(', ')
+      if (!values) return 0
+      const translateX = parseFloat(values[4])
+      return translateX
+    }
+    return 0 // 如果没有 transform 或 translateX,默认返回 0
+  }
+
+  // 检查并更新箭头显示状态
+  const updateArrowVisibility = () => {
+    const translateX = getTranslateX()
+    if (translateX === 0) {
+      leftArrow.style.display = 'none'
+    } else {
+      leftArrow.style.display = 'inline-block'
+    }
+
+    if (translateX === rightArrowHideDistance.value) {
+      rightArrow.style.display = 'none'
+    } else {
+      rightArrow.style.display = 'inline-block'
+    }
+  }
+
+  // 监听 transitionend 事件,等待动画结束后再获取 translateX 值
+  targetObserverElement.addEventListener('transitionend', (event: any) => {
+    if (event.propertyName === 'transform') {
+      // 只有 transform 动画结束时才触发
+      updateArrowVisibility()
+    }
+  })
+
+  // 初次运行时手动检查一次
+  updateArrowVisibility()
+}
+
+// 筛选选中时滚动到对应的元素
+const handleDocumentClick = (event: any) => {
+  if (!scrollTargetElement.value.contains(event.target)) {
+    scrollTargetElement.value.className = scrollTargetElement.value.className.replace(
+      'search-select-item',
+      ''
+    )
+    scrollTargetElement.value = null
+    document.removeEventListener('click', handleDocumentClick)
+  }
+}
+const scrollTargetElement = ref()
+const scrollToItem = (itemId: string) => {
+  if (activeName.value !== 'All') {
+    activeName.value = 'All'
+  }
+  setTimeout(() => {
+    // 重置
+    if (scrollTargetElement.value) {
+      scrollTargetElement.value.className = scrollTargetElement.value.className.replace(
+        'search-select-item',
+        ''
+      )
+    }
+    // 获取目标元素
+    scrollTargetElement.value = document.querySelector(`[data-field='${itemId}']`)
+    if (scrollTargetElement.value) {
+      // 使用 scrollIntoView 滚动到该元素
+      scrollTargetElement.value.scrollIntoView({ behavior: 'smooth', block: 'center' })
+      scrollTargetElement.value.className += ' search-select-item'
+      // 或者使用自定义滚动
+      // const container = this.$refs.dataContainer
+      // container.scrollTop = targetElement.offsetTop - container.offsetTop
+
+      document.addEventListener('click', handleDocumentClick)
+    }
+  }, 100)
+}
+
+// 系统首次加载时,会有引导操作
+let firstLoad = ref()
+const step1 = ref()
+const open1 = ref(false)
+const isShowStep1 = ref(false)
+const step2 = ref()
+const open2 = ref(false)
+const isShowStep2 = ref(false)
+const handleCloseTour = (stepStr: string) => {
+  if (stepStr === 'step1') {
+    isShowStep1.value = false
+    open1.value = false
+  } else {
+    isShowStep2.value = false
+    open2.value = false
+  }
+  localStorage.setItem('firstLoadCustomizeColumns', 'true')
+  // firstLoad = 'true'
+}
+
+// 左侧选中的tab
+const activeName = ref()
+// 分组列
+const groupColumns: any = ref([])
+// 所有数据
+const allDataCopy: any = ref()
+
+const loading = ref(false)
+// 获取数据
+const getData = async (selectedList?: Array<any>) => {
+  loading.value = true
+  let paramsData: any = { ...params.value }
+
+  await $api.getReportFieldsConfiguration(paramsData).then((res: any) => {
+    if (res.code === 200) {
+      // allDataCopy就是所有的数据
+      allDataCopy.value = res.data.GroupColumnsAll
+      groupColumns.value = res.data.GroupColumnsLeft
+      activeName.value = allDataCopy.value?.[0]?.name
+      searchOptions.value = res.data.GroupColumnsLeft?.[0]?.children
+
+      // 右侧 当没有初始值时才需要从接口获取数据
+      if (selectedList?.length && selectedList.length !== 0) {
+        const newArr = selectedList.map((item: any) => {
+          return {
+            ...item,
+            label: item.title
+          }
+        })
+        selectColumns.value = newArr
+      } else {
+        selectColumns.value = res.data.GroupColumnsRight
+      }
+      nextTick(() => {
+        handleTabArrow()
+        // 八秒后关闭引导
+        if (!firstLoad.value) {
+          setTimeout(() => {
+            handleCloseTour('step1')
+            handleCloseTour('step2')
+          }, 8000)
+        }
+      })
+    }
+  })
+  loading.value = false
+}
+
+const params = ref()
+// rightDistance是右侧箭头消失所需要translateX的值
+const openDialog = async (
+  paramsData: Object,
+  rightDistance: number,
+  tips: string,
+  selectedList?: Array<any>
+) => {
+  firstLoad.value = localStorage.getItem('firstLoadCustomizeColumns')
+  params.value = paramsData
+  dialogVisible.value = true
+
+  dragTips.value = tips
+  await getData(selectedList)
+  rightArrowHideDistance.value = rightDistance
+  nextTick(() => {
+    if (!firstLoad.value) {
+      open1.value = true
+      isShowStep1.value = true
+      open2.value = true
+      isShowStep2.value = true
+    }
+  })
+}
+
+const selectColumns: any = ref([])
+
+// 左侧Icon的显隐
+const hoverAllIcon = ref('')
+// 右侧Icon的显隐
+const hoverSelectIcon = ref('')
+
+const handleAddSelect = (item: any) => {
+  groupColumns.value.forEach((groupItem: any) => {
+    groupItem.children.forEach((child: any, index: number) => {
+      if (child.field === item.field) {
+        groupItem.children.splice(index, 1)
+      }
+    })
+  })
+  selectColumns.value.push(item)
+}
+
+// 从左侧拖拽到右侧时,删除其他分组中相同的数据
+const handleLeftRemove = (e: any) => {
+  if (e.to === e.from) return
+  const curItem = e.data
+  groupColumns.value.forEach((groupItem: any) => {
+    groupItem.children.forEach((child: any, index: number) => {
+      if (child.field === curItem.field) {
+        groupItem.children.splice(index, 1)
+      }
+    })
+  })
+}
+
+// 从右侧拖拽到左侧时,左侧根据分组添加数据
+const handleRightRemove = (e: any) => {
+  if (e.to === e.from) return
+  const curItem = e.data
+  // 获取当前移动项移入到了那一组
+  const curGroup = groupColumns.value.find((item: any) => {
+    return item.name == activeName.value
+  })
+  // 获取当前项应该对应哪一组
+  const originalGroup = allDataCopy.value.find((item: any) => {
+    if (item.name === 'All') {
+      return false
+    }
+    const index = item.children.findIndex((child: any) => {
+      return child.field === curItem.field
+    })
+    return index !== -1
+  })
+
+  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)
+    })
+    // 添加到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)
+    })
+  } else if (curGroup.name === originalGroup?.name) {
+    groupColumns.value[0].children.push(curItem)
+  }
+}
+// 点击右侧的减号删除选中的列,并添加到左侧
+const handleDeleteSelect = (curItem: any) => {
+  selectColumns.value.forEach((item: any, index: number) => {
+    if (item.field === curItem.field) {
+      selectColumns.value.splice(index, 1)
+    }
+  })
+  // 获取当前项应该对应哪一组
+  const originalGroup = allDataCopy.value.find((item: any) => {
+    if (item.name === 'All') {
+      return false
+    }
+    const index = item.children.findIndex((child: any) => {
+      return child.field === curItem.field
+    })
+    return index !== -1
+  })
+  // 在对应分组中添加移入的数据
+  groupColumns.value.forEach((item: any) => {
+    item.name === originalGroup?.name && item.children.push(curItem)
+  })
+  // 添加到All分组里
+  groupColumns.value[0].children.push(curItem)
+}
+
+const handleMoveUpSelect = (item: any) => {
+  const index = selectColumns.value.findIndex((i: any) => i.field === item.field)
+  if (index === 0) return
+  const temp = selectColumns.value[index]
+  selectColumns.value[index] = selectColumns.value[index - 1]
+  selectColumns.value[index - 1] = temp
+}
+const handleMoveDownSelect = (item: any) => {
+  const index = selectColumns.value.findIndex((i: any) => i.field === item.field)
+  if (index === selectColumns.value.length - 1) return
+  const temp = selectColumns.value[index]
+  selectColumns.value[index] = selectColumns.value[index + 1]
+  selectColumns.value[index + 1] = temp
+}
+
+const emits = defineEmits<{
+  apply: [selectColumns: any]
+  reset: []
+}>()
+
+const handleApply = () => {
+  emits('apply', selectColumns.value)
+  dialogVisible.value = false
+}
+
+const clearData = () => {
+  open1.value = false
+  open2.value = false
+  activeName.value = ''
+  groupColumns.value = []
+  selectColumns.value = []
+  searchColumn.value = ''
+}
+
+defineExpose({
+  openDialog
+})
+</script>
+
+<template>
+  <el-dialog
+    class="customize-columns"
+    v-model="dialogVisible"
+    :width="1000"
+    title="Add/Edit Field"
+    @close="clearData"
+  >
+    <div class="search-header">
+      <div class="search-input" ref="searchRef">
+        <el-select
+          v-model="searchColumn"
+          @change="scrollToItem"
+          filterable
+          placeholder="Search field"
+        >
+          <template #prefix>
+            <span class="font_family icon-icon_search_b"></span>
+          </template>
+          <el-option
+            v-for="item in searchOptions"
+            :key="item.field"
+            :label="item.label"
+            :value="item.field"
+          />
+        </el-select>
+      </div>
+      <div class="tips">
+        <span style="font-size: 16px">* </span>
+        <span>{{ dragTips }}</span>
+      </div>
+    </div>
+    <div class="draggable-list">
+      <div class="left-all-columns" v-vloading="loading">
+        <div class="tabs">
+          <el-tabs v-model="activeName">
+            <el-tab-pane
+              v-for="groupItem in groupColumns"
+              :key="groupItem.name"
+              :label="groupItem.name"
+              :name="groupItem.name"
+            >
+              <VueDraggable
+                v-model="groupItem.children"
+                class="column-list"
+                ghost-class="ghost-column"
+                :forceFallback="true"
+                fallbackClass="fallback-class"
+                group="customizeColumns"
+                item-key="field"
+                @end="handleLeftRemove"
+              >
+                <template v-for="(item, index) in groupItem.children" :key="item.field">
+                  <div
+                    :data-field="item.field"
+                    class="column-item"
+                    @mouseenter="hoverAllIcon = item.field"
+                    @mouseleave="hoverAllIcon = ''"
+                  >
+                    <span class="font_family icon-icon_dragsort__b draggable-icon"></span>
+                    <span class="title">{{ item.label }}</span>
+                    <span
+                      ref="step1"
+                      v-if="hoverAllIcon === item.field || (index === 0 && isShowStep1)"
+                      class="font_family icon-icon_add_b move-icon"
+                      @click="handleAddSelect(item)"
+                    ></span>
+                  </div>
+                </template>
+              </VueDraggable>
+            </el-tab-pane>
+          </el-tabs>
+        </div>
+      </div>
+      <div class="right-select-columns">
+        <div class="title">Selected fields on report template list</div>
+        <VueDraggable
+          v-vloading="loading"
+          v-model="selectColumns"
+          class="column-list"
+          ghost-class="ghost-column"
+          :forceFallback="true"
+          fallback-class="fallback-class"
+          group="customizeColumns"
+          item-key="field"
+          @end="handleRightRemove"
+        >
+          <template v-for="(item, index) in selectColumns" :key="item.field">
+            <div
+              class="column-item"
+              @mouseenter="hoverSelectIcon = item.field"
+              @mouseleave="hoverSelectIcon = ''"
+            >
+              <span
+                class="font_family icon-icon_dragsort__b draggable-icon"
+                style="font-size: 16px"
+              ></span>
+              <span class="title">{{ item.label }}</span>
+              <span
+                v-if="hoverSelectIcon === item.field || (index === 0 && isShowStep2)"
+                class="font_family icon-icon_moveup_b move-icon"
+                @click="handleMoveUpSelect(item)"
+              ></span>
+              <span
+                v-if="hoverSelectIcon === item.field || (index === 0 && isShowStep2)"
+                class="font_family icon-icon_movedown_b move-icon"
+                @click="handleMoveDownSelect(item)"
+              ></span>
+              <span
+                ref="step2"
+                v-if="hoverSelectIcon === item.field || (index === 0 && isShowStep2)"
+                class="font_family icon-icon_reduce_b move-icon"
+                @click="handleDeleteSelect(item)"
+              ></span>
+            </div>
+          </template>
+        </VueDraggable>
+      </div>
+    </div>
+    <template #footer>
+      <el-button
+        type="default"
+        style="height: 40px; padding: 8px 40px"
+        @click="dialogVisible = false"
+        >Cancel</el-button
+      >
+      <el-button
+        class="el-button--dark"
+        style="height: 40px; padding: 8px 40px"
+        @click="handleApply"
+      >
+        Apply
+      </el-button>
+    </template>
+    <el-tour
+      :target-area-clickable="false"
+      class="step1-tour"
+      v-model="open1"
+      :mask="false"
+      type="primary"
+      v-if="step1?.[0]"
+    >
+      <el-tour-step :show-close="false" :target="step1?.[0]">
+        <template #default>
+          <div class="description">
+            <span>Drag</span> items to the right group or click the "<span>Add</span>" icon to add
+            columns to the {{ route.path.includes('booking') ? 'booking' : 'shipment' }} list.
+          </div>
+          <div class="got-it-text" @click="handleCloseTour('step1')">Got it</div>
+        </template>
+      </el-tour-step>
+    </el-tour>
+    <el-tour
+      :target-area-clickable="false"
+      class="step2-tour"
+      v-model="open2"
+      type="primary"
+      :mask="false"
+      v-if="step2?.[0]"
+    >
+      <el-tour-step :show-close="false" :target="step2?.[0]">
+        <template #default>
+          <div class="description">
+            <span>Drag</span> items to the left group or click the "<span>Remove</span>" icon to
+            delete columns from the
+            {{ route.path.includes('booking') ? 'booking' : 'shipment' }} list.
+          </div>
+          <div class="description">
+            <span>Drag</span> items up or down to reorder the
+            {{ route.path.includes('booking') ? 'booking' : 'shipment' }} list, or use the "<span
+              >Move up</span
+            >" and "<span>Move down</span>" icons.
+          </div>
+          <div class="got-it-text" @click="handleCloseTour('step2')">Got it</div>
+        </template>
+      </el-tour-step>
+    </el-tour>
+  </el-dialog>
+</template>
+
+<style lang="scss">
+.customize-columns {
+  .search-header {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    gap: 8px;
+    padding: 10px 0;
+
+    .search-input {
+      width: 50%;
+      padding-right: 16px;
+    }
+
+    .tips {
+      display: flex;
+      align-items: flex-start;
+      gap: 3px;
+      width: 50%;
+      padding-left: 5px;
+      vertical-align: middle;
+
+      span {
+        font-size: 12px;
+        color: var(--color-neutral-2);
+      }
+    }
+  }
+
+  .draggable-list {
+    display: flex;
+    user-select: none;
+    gap: 8px;
+  }
+}
+
+.right-select-columns,
+.left-all-columns {
+  width: 50%;
+
+  .column-list {
+    height: 400px;
+    overflow: auto;
+
+    .column-item {
+      display: flex;
+      align-items: center;
+      height: 40px;
+      margin-bottom: 5px;
+      padding-left: 12px;
+      border: 1px solid var(--color-border);
+      border-radius: 6px;
+      background-color: var(--color-customize-column-item-bg);
+
+      &:hover {
+        background-color: var(--color-customize-column-item-hover-bg);
+        box-shadow: 4px 4px 32px 0px rgba(0, 0, 0, 0.1);
+      }
+
+      & > .title {
+        flex: 1;
+      }
+
+      span.draggable-icon {
+        margin-right: 12px;
+        color: var(--color-customize-column-item-drag-icon);
+      }
+
+      .font_family {
+        font-size: 16px;
+        cursor: pointer;
+        margin-right: 16px;
+      }
+
+      .move-icon {
+        &:hover {
+          color: var(--color-theme);
+        }
+      }
+    }
+  }
+
+  .ghost-column {
+    cursor: move !important;
+    span {
+      opacity: 0;
+    }
+    border: 1px dashed var(--color-customize-column-item-drag-border) !important;
+    background-color: var(--color-customize-column-item-drag-bg) !important;
+    box-shadow: none !important;
+  }
+
+  .fallback-class {
+    opacity: 1 !important;
+    background-color: var(--color-customize-column-item-hover-bg) !important;
+    cursor: move !important;
+  }
+}
+
+.left-all-columns {
+  border: 1px solid var(--color-border);
+  border-radius: 12px;
+
+  .tabs {
+    position: relative;
+    height: 100%;
+
+    .el-tabs {
+      .el-tabs__header {
+        margin-bottom: 0px;
+        border-bottom: 1px solid var(--color-customize-column-tabs-header-border);
+      }
+
+      .el-tabs__item {
+        padding: 10px;
+      }
+    }
+  }
+
+  .column-list {
+    padding: 8px;
+    padding-bottom: 0px;
+  }
+
+  .search-select-item {
+    border: 1px solid var(--color-theme) !important;
+    box-shadow: 2px 2px 12px 0px rgba(237, 109, 0, 0.2);
+
+    .title {
+      color: var(--color-theme) !important;
+    }
+  }
+}
+
+.right-select-columns {
+  background-color: var(--color-customize-column-right-section-bg);
+  padding-top: 0;
+  border: 1px dashed var(--color-customize-column-right-section-border);
+  border-radius: 12px;
+
+  & > .title {
+    height: 40px;
+    padding: 8px;
+    line-height: 24px;
+    font-size: 16px;
+    font-weight: 700;
+  }
+
+  .column-list {
+    padding: 8px;
+    padding-bottom: 0px;
+  }
+}
+</style>
+<style lang="scss">
+.left-all-columns {
+  .el-tabs__nav-prev,
+  .el-tabs__nav-next {
+    height: 40px;
+    width: 40px;
+  }
+
+  .el-tabs__item {
+    color: var(--color-neutral-1);
+    font-weight: 400;
+    font-size: 14px;
+  }
+
+  .el-tabs__item.is-active,
+  .el-tabs__item:hover {
+    font-weight: 700;
+    font-size: 14px;
+    color: var(--color-neutral-1);
+  }
+
+  .el-tabs__nav-prev {
+    border-right: 1px solid var(--color-border);
+    box-shadow: 2px 0px 12px rgba(0, 0, 0, 0.3);
+    // .el-icon {
+    //   color: white;
+    // }
+    /* 左侧阴影 */
+  }
+
+  .el-tabs__nav-next {
+    border-left: 1px solid var(--color-border);
+    box-shadow: -2px 0px 12px rgba(0, 0, 0, 0.2);
+    /* 左侧阴影 */
+  }
+
+  .el-tabs__nav-wrap {
+    padding: 0 40px;
+  }
+
+  .el-tabs__item.is-active,
+  .el-tabs__item:hover {
+    color: var(--color-theme);
+  }
+
+  .el-tabs__active-bar {
+    background-color: var(--color-theme);
+  }
+}
+
+.search-header {
+  & > .search-input {
+    .el-select {
+      width: 100%;
+
+      .el-select__wrapper {
+        border-radius: 20px;
+      }
+    }
+  }
+}
+</style>
+
+<style lang="scss">
+.step1-tour {
+  .el-tour__content {
+    width: 240px;
+    height: 124px;
+    background-color: var(--color-theme);
+    z-index: 9999 !important;
+  }
+
+  .el-tour__arrow {
+    background-color: var(--color-theme);
+  }
+
+  .el-tour__footer {
+    display: none;
+  }
+}
+
+.step2-tour {
+  .el-tour__content {
+    width: 240px;
+    height: 200px;
+    background-color: var(--color-theme);
+    z-index: 9999 !important;
+  }
+
+  .el-tour__arrow {
+    background-color: var(--color-theme);
+  }
+
+  .el-tour__footer {
+    display: none;
+  }
+}
+
+.step1-tour,
+.step2-tour {
+  .el-tour__header {
+    display: none;
+  }
+
+  .description {
+    margin-bottom: 16px;
+    color: white;
+    line-height: 22px;
+
+    span {
+      color: white;
+      font-weight: 600;
+    }
+  }
+
+  .got-it-text {
+    float: right;
+    color: white;
+    font-weight: 700;
+    cursor: pointer;
+  }
+}
+</style>

+ 185 - 0
src/views/TemplateManagement/src/components/CreateReportTemplate/src/components/GroupNameSelect.vue

@@ -0,0 +1,185 @@
+<script setup lang="ts">
+import { cloneDeep, debounce } from 'lodash'
+import axios from 'axios'
+
+const props = defineProps({
+  data: {
+    type: Array as () => string[],
+    default: () => []
+  }
+})
+
+const selectViewRef = ref(null)
+const selectData = ref<string[]>([])
+watch(
+  () => props.data,
+  () => {
+    selectData.value = cloneDeep(props.data) || []
+  },
+  {
+    deep: true,
+    immediate: true
+  }
+)
+const emit = defineEmits(['changeData'])
+const changeData = (val: string[]) => {
+  // 同步选中状态
+  emit('changeData', val)
+}
+const handleVisibleChange = (visible) => {
+  !visible && (options.value = [])
+}
+
+interface ListItem {
+  id: string
+  label: string
+  checked: boolean
+}
+const options = ref<ListItem[]>([])
+const loading = ref(false)
+
+onMounted(() => {
+  options.value = []
+})
+
+const currentController = ref<AbortController | null>(null)
+// 搜索方法
+const remoteMethod = (query: string) => {
+  currentController.value?.abort()
+
+  const newController = new AbortController()
+  currentController.value = newController
+  loading.value = true
+  $api
+    .getSpecificRolesGroupName({ term: query }, { signal: newController.signal })
+    .then((res: any) => {
+      if (!newController.signal.aborted && res.code == 200) {
+        options.value = (res.data || []).map((item: any) => ({
+          id: item.id,
+          label: item.label,
+          code: item.code,
+          checked: false
+        }))
+      } else {
+        options.value = []
+      }
+    })
+    .catch((err) => {
+      options.value = []
+      if (!axios.isCancel(err) && !newController.signal.aborted) {
+        ElMessage.error('Failed to load options')
+      }
+    })
+    .finally(() => {
+      // 仅当这是最新请求时,才关闭 loading
+      if (currentController.value === newController) {
+        loading.value = false
+      }
+    })
+}
+
+// 防抖版本(可选)
+const debouncedRemoteMethod = debounce(remoteMethod, 200)
+
+onUnmounted(() => {
+  currentController.value?.abort()
+})
+</script>
+
+<template>
+  <el-select
+    v-model="selectData"
+    multiple
+    filterable
+    reserve-keyword
+    placeholder="Select Group Name (Multi-select allowed)"
+    :loading="loading"
+    style="width: 100%"
+    ref="selectViewRef"
+    popper-class="group-name-select-popper"
+    :filter-method="debouncedRemoteMethod"
+    @change="changeData"
+    @visible-change="handleVisibleChange"
+  >
+    <el-option
+      v-for="item in options"
+      :key="item.id + item.label"
+      :label="item.label"
+      :value="item.label"
+    >
+      <!-- 只用于显示,不控制逻辑 -->
+      <el-checkbox
+        class="checkbox-style"
+        :model-value="selectData.includes(item.label)"
+        style="flex: 1"
+      >
+        <div class="select-option">
+          <span>{{ item.label }}</span>
+          <span class="value" :style="{ width: selectViewRef?.$el?.offsetWidth - 70 + 'px' }">
+            {{ item.id }}
+          </span>
+        </div>
+      </el-checkbox>
+    </el-option>
+  </el-select>
+</template>
+
+<style lang="scss" scoped>
+.select-option {
+  flex: 1;
+  display: flex;
+  align-items: center;
+  flex-direction: column;
+  align-items: flex-start;
+  max-width: 100%;
+  gap: 3px;
+  padding-top: 1px;
+
+  min-height: 32px;
+  max-height: 80px;
+  :deep(.el-checkbox__label) {
+    font-weight: 700;
+  }
+  & > .value {
+    overflow: auto;
+    white-space: wrap;
+    font-size: 12px;
+    line-height: 16px;
+  }
+}
+.value {
+  color: var(--color-neutral-2);
+}
+.checkbox-style {
+  min-height: 32px;
+  max-height: 80px;
+  height: auto;
+  :deep(.el-checkbox__label) {
+    flex: 1;
+    overflow: auto;
+  }
+}
+:deep(.el-checkbox__inner) {
+  height: 16px;
+  width: 16px;
+  &::after {
+    border-width: 2px;
+    height: 9px;
+    width: 5px;
+    left: 4px;
+    top: 0;
+  }
+}
+</style>
+
+<style lang="scss">
+.group-name-select-popper {
+  .el-select-dropdown__item {
+    height: auto;
+    padding: 8px;
+  }
+  .el-checkbox__input {
+    align-self: flex-start;
+  }
+}
+</style>

+ 149 - 0
src/views/TemplateManagement/src/components/CreateReportTemplate/src/components/PartyIDSelect.vue

@@ -0,0 +1,149 @@
+<script setup lang="ts">
+import { cloneDeep, debounce } from 'lodash'
+import axios from 'axios'
+
+const props = defineProps({
+  data: {
+    type: Array as () => string[],
+    default: () => []
+  }
+})
+
+const selectData = ref<string[]>([])
+watch(
+  () => props.data,
+  (newValue) => {
+    selectData.value = cloneDeep(newValue) || []
+  },
+  {
+    deep: true,
+    immediate: true
+  }
+)
+const emit = defineEmits(['changeData'])
+const changeData = (val: string[]) => {
+  // 同步选中状态
+  emit('changeData', val)
+}
+
+interface ListItem {
+  label: string
+  id: string
+}
+
+const options = ref<ListItem[]>([])
+const loading = ref(false)
+const currentController = ref<AbortController | null>(null)
+
+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()
+})
+
+// 首次聚焦或输入时加载(可选:如果希望空搜也加载)
+// 但通常 remote 场景是“输入才搜”,所以这里只在 filter 时调用
+</script>
+
+<template>
+  <el-select
+    :model-value="selectData"
+    multiple
+    filterable
+    reserve-keyword
+    placeholder="Select Party IDs (Multi-select allowed)"
+    :loading="loading"
+    style="width: 100%"
+    popper-class="part-id-select-popper"
+    :filter-method="debouncedRemoteMethod"
+    @change="changeData"
+  >
+    <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="selectData.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>
+</template>
+
+<style lang="scss" scoped>
+.select-option {
+  display: flex;
+  align-items: center;
+  gap: 16px;
+  width: 100%;
+  & > span {
+    margin-left: 56px;
+    line-height: 16px;
+  }
+}
+
+:deep(.el-checkbox__inner) {
+  height: 16px;
+  width: 16px;
+  &::after {
+    border-width: 2px;
+    height: 9px;
+    width: 5px;
+    left: 4px;
+    top: 0;
+  }
+}
+:deep(.el-checkbox__label) {
+  flex: 1;
+  display: flex;
+}
+.text-ellipsis {
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  overflow: hidden;
+}
+</style>
+
+<style lang="scss">
+.part-id-select-popper {
+  // width: 100% !important;
+  // width: auto;
+  // min-width: unset !important;
+}
+</style>

+ 1 - 0
src/views/TemplateManagement/src/components/TableView/index.ts

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

+ 457 - 0
src/views/TemplateManagement/src/components/TableView/src/TableView.vue

@@ -0,0 +1,457 @@
+<script setup lang="ts">
+import { ref, nextTick, onMounted } from 'vue'
+import { type VxeGridInstance, type VxeGridProps } from 'vxe-table'
+import { useRowClickStyle } from '@/hooks/rowClickStyle'
+import dayjs from 'dayjs'
+import utc from 'dayjs/plugin/utc'
+import timezone from 'dayjs/plugin/timezone'
+import { formatNumber } from '@/utils/tools'
+import { useRouter } from 'vue-router'
+import { useUserStore } from '@/stores/modules/user'
+
+dayjs.extend(utc)
+dayjs.extend(timezone)
+const router = useRouter()
+const props = defineProps({
+  height: {
+    type: Number,
+    default: 440
+  }
+})
+
+const userStore = useUserStore()
+const formatString = computed(() => {
+  return userStore.dateFormat || 'MM/DD/YYYY'
+})
+
+const tableOriginColumnsField = ref()
+const tableColumns = [
+  {
+    title: 'Report Name',
+    type: 'normal',
+    field: 'name'
+  },
+  {
+    title: 'Report Level',
+    type: 'normal',
+    field: 'level'
+  },
+  {
+    title: 'Is Active',
+    type: 'status',
+    field: 'is_active'
+  },
+  {
+    title: 'Application Scope',
+    type: 'normal',
+    field: 'access_type'
+  },
+  {
+    title: 'Creation Date',
+    type: 'normal',
+    field: 'created_time',
+    formatter: 'dateTime',
+    sortable: true
+  }
+]
+const handleColumns = (columns: any) => {
+  const newColumns = columns.map((item: any) => {
+    let curColumn: any = {
+      title: item.title,
+      field: item.field,
+      sortable: item.sortable || false,
+      minWidth: 120,
+      showOverflow: true
+    }
+    // 设置插槽
+    if (item.type === 'status') {
+      curColumn = {
+        ...curColumn,
+        slots: { default: 'status' }
+      }
+    } else if (item.type === 'link') {
+      curColumn = {
+        ...curColumn,
+        slots: { default: 'link' }
+      }
+    } else if (item.type === 'mode') {
+      curColumn = {
+        ...curColumn,
+        slots: { default: 'mode' },
+        formatter: ({ cellValue }: any) => {
+          return cellValue
+        }
+      }
+    }
+    // 格式化
+    if (item.formatter === 'date' || item.formatter === 'dateTime') {
+      curColumn = {
+        ...curColumn,
+        formatter: ({ cellValue }: any) =>
+          dayjs.tz(cellValue, 'US/Pacific').format(formatString.value + ' HH:mm:ss ')
+      }
+    }
+    return curColumn
+  })
+  return newColumns
+}
+
+// 获取表格列
+const getTableColumns = async () => {
+  tableData.value.columns = [
+    { title: 'Action', width: 120, fixed: 'left', slots: { default: 'action' } },
+    ...handleColumns(tableColumns)
+  ]
+  // tableRef.value && autoWidth(tableData.value, tableRef.value)
+}
+
+const handleClick = (row: any, isActive: boolean) => {
+  $api
+    .changeReportTemplateIsActive({
+      serial_no: row.serial_no,
+      is_active: isActive
+    })
+    .then((res: any) => {
+      if (res.code === 200) {
+        row.is_active = isActive ? 't' : 'f'
+        // getTableData(true)
+      }
+    })
+}
+
+const pageInfo = ref({ pageNo: 1, pageSize: 20, total: 0 })
+const isShowDeleteBtn = ref(false)
+// 获得表格数据后赋值
+const assignTableData = (data: any) => {
+  tableData.value.data = data.searchData || []
+  pageInfo.value.total = Number(data.rc) || 0
+  isShowDeleteBtn.value = data?.isDelete || false
+  const actionColumn = tableData.value.columns.find((item) => {
+    return item.title === 'Action'
+  })
+  actionColumn.width = isShowDeleteBtn.value ? 150 : 120
+  tableRef.value.loadColumn(tableData.value.columns)
+}
+
+let searchdata: any = {}
+// 获取表格数据
+const getTableData = async (isPageChange?: boolean) => {
+  const rc = isPageChange ? pageInfo.value.total : -1
+  tableLoadingTableData.value = true
+  await $api
+    .getReportTemplateManagementTable({
+      cp: pageInfo.value.pageNo,
+      ps: pageInfo.value.pageSize,
+      rc,
+      ...searchdata
+    })
+    .then((res: any) => {
+      if (res.code === 200) {
+        assignTableData(res.data)
+      }
+    })
+    .finally(() => {
+      nextTick(() => {
+        // tableRef.value && autoWidth(tableData.value, tableRef.value)
+        tableLoadingTableData.value = false
+      })
+    })
+}
+const searchTableData = (val: any) => {
+  searchdata = val
+  tableLoadingTableData.value = true
+  $api
+    .getReportTemplateManagementTable({
+      cp: pageInfo.value.pageNo,
+      ps: pageInfo.value.pageSize,
+      rc: -1,
+      ...val
+    })
+    .then((res: any) => {
+      if (res.code === 200) {
+        assignTableData(res.data)
+      }
+    })
+    .finally(() => {
+      nextTick(() => {
+        // tableRef.value && autoWidth(tableData.value, tableRef.value)
+        tableLoadingTableData.value = false
+      })
+    })
+}
+onMounted(() => {
+  Promise.all([getTableColumns(), getTableData(false)]).finally(() => {
+    nextTick(() => {
+      // tableRef.value && autoWidth(tableData.value, tableRef.value)
+    })
+  })
+})
+
+const tableRef = ref<VxeGridInstance>()
+const tableData = ref<VxeGridProps<any>>({
+  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)'
+  },
+  cellConfig: {
+    height: 40
+  },
+  headerCellConfig: {
+    height: 40
+  },
+  sortConfig: {
+    sortMethod: (params) => {
+      const { data, sortList } = params
+
+      // 如果没有排序条件,直接返回原数据
+      if (sortList.length === 0) return data
+
+      // 对数据进行多重排序
+      const sortedData = [...data].sort((a, b) => {
+        for (const { field, order } of sortList) {
+          const curColumn = tableOriginColumnsField.value.find((item: any) => item.field === field)
+          if (!curColumn) continue
+
+          const typeName = curColumn.type
+          const aValue = a[field]
+          const bValue = b[field]
+
+          const compareResult = (aValue: any, bValue: any) => {
+            // 如果 aValue 或 bValue 是 null 或 undefined,优先处理这些值
+            if (aValue == null && bValue == null) {
+              return 0 // 如果两个值都为 null 或 undefined,视为相等
+            } else if (aValue == null) {
+              return -1 // 如果 aValue 是 null,bValue 不是,则将 aValue 视为较小值
+            } else if (bValue == null) {
+              return 1 // 如果 bValue 是 null,aValue 不是,则将 bValue 视为较小值
+            }
+
+            if (typeName === 'datetime' || typeName === 'date' || typeName === 'time') {
+              return dayjs(aValue).unix() - dayjs(bValue).unix()
+            } else if (isNaN(Number(aValue)) || isNaN(Number(bValue))) {
+              return aValue.localeCompare(bValue)
+            } else {
+              return Number(aValue) - Number(bValue)
+            }
+          }
+
+          const result = compareResult(aValue, bValue)
+          if (result !== 0) {
+            return order === 'asc' ? result : -result
+          }
+        }
+
+        return 0 // 如果所有字段都相等
+      })
+
+      return sortedData
+    }
+  },
+  columnConfig: { resizable: true, useKey: true },
+  rowConfig: { isHover: true },
+  exportConfig: {
+    types: ['csv', 'html', 'txt', 'xlsx'],
+    modes: ['current', 'selected', 'all']
+  }
+})
+
+const handleEdit = (serial_no: string) => {
+  router.push({
+    name: 'Create Report Template',
+    query: { serial_no }
+  })
+}
+
+const handleCopy = (serial_no: string) => {
+  router.push({
+    name: 'Create Report Template',
+    query: { serial_no, copy: 't' }
+  })
+}
+
+const handleDelete = (serial_no: string) => {
+  $api.deleteReportTemplate({ serial_no }).then((res: any) => {
+    if (res.code === 200) {
+      getTableData()
+    }
+  })
+}
+
+// 实现行点击样式
+useRowClickStyle(tableRef)
+
+const tableLoadingColumn = ref(false)
+const tableLoadingTableData = ref(false)
+
+const handleCreate = () => {
+  router.push({
+    name: 'Create Report Template'
+  })
+}
+
+defineExpose({
+  searchTableData
+})
+</script>
+
+<template>
+  <div class="table-box">
+    <vxe-grid
+      ref="tableRef"
+      v-vloading="tableLoadingTableData || tableLoadingColumn"
+      :height="props.height"
+      :style="{ border: 'none' }"
+      v-bind="tableData"
+    >
+      <!-- action操作栏的插槽 -->
+      <template #action="{ row }">
+        <el-button class="el-button--blue action-btn" @click="handleEdit(row.serial_no)">
+          <span
+            style="margin-right: 2px; font-size: 15px"
+            class="font_family icon-icon_edit_b"
+          ></span>
+        </el-button>
+        <el-button class="el-button--blue action-btn" @click="handleCopy(row.serial_no)">
+          <span
+            style="margin-right: 2px; font-size: 15px"
+            class="font_family icon-icon_clone_b"
+          ></span>
+        </el-button>
+        <el-button
+          class="el-button--blue action-btn"
+          v-if="row.is_active === 't'"
+          @click="handleClick(row, false)"
+        >
+          <span
+            style="margin-right: 2px; font-size: 16px"
+            class="font_family icon-icon_disablee__b"
+          ></span>
+        </el-button>
+        <el-button
+          class="el-button--blue action-btn"
+          v-if="row.is_active === 'f'"
+          @click="handleClick(row, true)"
+        >
+          <span
+            style="margin-right: 2px; font-size: 13px"
+            class="font_family icon-icon_active"
+          ></span>
+        </el-button>
+        <el-button
+          v-if="isShowDeleteBtn"
+          class="el-button--blue action-btn"
+          @click="handleDelete(row.serial_no)"
+        >
+          <span class="font_family icon-icon_delete_b"></span>
+        </el-button>
+      </template>
+      <!-- Status字段的插槽 -->
+      <template #status="{ row, column }">
+        <VTag :type="row[column.field] === 't' ? 'Active' : 'Inactive'">{{
+          row[column.field] === 't' ? 'Active' : 'Inactive'
+        }}</VTag>
+      </template>
+      <!-- 空数据时的插槽 -->
+      <template #empty v-if="!tableLoadingTableData && tableData.data.length === 0">
+        <div class="empty-box">
+          <el-button class="el-button--dark" @click="handleCreate">
+            <span class="font_family icon-icon_add_b"></span> Create New Report Template</el-button
+          >
+          <p>Click the "Create New Report Template" button to add a report template.</p>
+        </div>
+      </template>
+    </vxe-grid>
+    <div class="pagination">
+      <span>Total {{ formatNumber(pageInfo.total) }}</span>
+      <el-pagination
+        v-model:current-page="pageInfo.pageNo"
+        v-model:page-size="pageInfo.pageSize"
+        :page-sizes="[50, 100, 200, 300, 400]"
+        :pagerCount="5"
+        background
+        layout="sizes, prev, pager, next"
+        :total="pageInfo.total"
+        @size-change="getTableData(true)"
+        @current-change="getTableData(true)"
+      />
+    </div>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.table-tools {
+  position: relative;
+  display: flex;
+  justify-content: space-between;
+  height: 48px;
+  padding: 8px 0;
+
+  .left-total-records {
+    font-size: 16px;
+    font-weight: 700;
+    line-height: 32px;
+  }
+}
+
+.bottom-pagination {
+  display: flex;
+  justify-content: space-between;
+  height: 40px;
+  margin-top: -1px;
+  padding: 4px 8px;
+  border: 1px solid var(--color-border);
+  border-radius: 0 0 12px 12px;
+
+  .left-total-records {
+    line-height: 32px;
+  }
+
+  .right-pagination {
+    display: flex;
+    align-items: center;
+  }
+}
+.table-box {
+  padding: 8px 20px 0;
+  position: relative;
+  overflow: hidden;
+
+  .all-table {
+    position: absolute;
+    top: -100000px;
+    width: 20px;
+  }
+  .empty-box {
+    text-align: center;
+    p {
+      color: var(--color-neutral-2);
+      margin-top: 12px;
+    }
+  }
+  .action-btn {
+    height: 24px;
+    width: 26px;
+    padding: 8px 4px;
+    padding-left: 5px;
+    font-size: 12px;
+  }
+}
+.pagination {
+  display: flex;
+  justify-content: space-between;
+  font-weight: 400;
+  font-size: 15px;
+  align-items: center;
+  border: 1px solid var(--color-border);
+  border-top: none;
+  padding: 4px 8px;
+  border-radius: 0 0 6px 6px;
+}
+</style>

+ 1 - 0
src/views/Tracking/src/components/DownloadAttachment/index.ts

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

+ 616 - 0
src/views/Tracking/src/components/DownloadAttachment/src/DownloadAttachment.vue

@@ -0,0 +1,616 @@
+<script setup lang="ts">
+import { useTrackingDownloadData } from '@/stores/modules/trackingDownloadData'
+import emitter from '@/utils/bus'
+import { useRouter } from 'vue-router'
+import { useUserStore } from '@/stores/modules/user'
+
+const userStore = useUserStore()
+const router = useRouter()
+const trackingDownloadData = useTrackingDownloadData()
+const attachmentData = ref([])
+const bodyLoading = ref(false)
+const pageLoading = ref(false)
+
+// const shipments = ref(attachmentData)
+const getAttachmentData = () => {
+  pageLoading.value = true
+  $api
+    .getDownloadAttachmentData({
+      serial_no_arr: trackingDownloadData.serialNoArr,
+      schemas_arr: trackingDownloadData.schemasArr
+    })
+    .then((res: any) => {
+      if (res.code === 200) {
+        attachmentData.value = res.data
+      }
+    })
+    .finally(() => {
+      pageLoading.value = false
+    })
+}
+onMounted(() => {
+  getAttachmentData()
+})
+
+// === 1. 全选状态计算 ===
+const isAllSelected = computed({
+  get() {
+    return attachmentData.value.every((item) => item.isSelect || item.typeList?.length === 0)
+  },
+  set(val) {
+    attachmentData.value.forEach((item) => {
+      if (item.typeList?.length === 0) return
+      item.isSelect = val
+      // 同步子级
+      if (item.typeList) {
+        item.typeList.forEach((type) => {
+          if (type.attachmentList) {
+            type.attachmentList.forEach((att) => {
+              att.isSelect = val
+            })
+          }
+        })
+      }
+    })
+  }
+})
+
+// 父级变化时,更新子级状态
+const handleParentToggle = (ship) => {
+  const newVal = ship.isSelect
+  ship.typeList.forEach((type) => {
+    if (type.attachmentList) {
+      type.attachmentList.forEach((att) => {
+        att.isSelect = newVal
+      })
+    }
+  })
+}
+
+// 子级变化时,更新父级状态
+const handleChildToggle = (ship) => {
+  if (!ship.typeList || ship.typeList.length === 0) {
+    // 如果没有子项,直接返回当前状态或设为 false
+    ship.isSelect = false
+    return
+  }
+
+  // 判断所有 attachment 是否都选中
+  const allSelected = ship.typeList.every((type) =>
+    type.attachmentList?.every((att) => att.isSelect)
+  )
+
+  ship.isSelect = allSelected
+}
+
+// === 3. 初始化数据结构(确保每个 attachment 都有 isSelect)
+// 如果原始数据不完整,可以预处理
+const initShipments = () => {
+  attachmentData.value.forEach((item) => {
+    if (!item.isSelect) item.isSelect = false
+    if (item.typeList) {
+      item.typeList.forEach((type) => {
+        if (type.attachmentList) {
+          type.attachmentList.forEach((att) => {
+            if (!att.isSelect) att.isSelect = false
+          })
+        }
+      })
+    }
+  })
+}
+
+initShipments()
+const summaryList = ref([])
+const allChooseFiles = computed(() => {
+  return summaryList.value.reduce((acc, curr) => {
+    acc += curr.attachmentList.length
+    return acc
+  }, 0)
+})
+
+const generateSummary = () => {
+  const map = new Map() // 用 label 作为 key
+
+  attachmentData.value.forEach((item) => {
+    item?.typeList?.forEach((type) => {
+      // 遍历该类型下的所有附件
+      type?.attachmentList?.forEach((attach) => {
+        if (attach.isSelect) {
+          const label = type.label
+          if (!map.has(label)) {
+            map.set(label, {
+              label,
+              number: 0,
+              attachmentList: []
+            })
+          }
+          const group = map.get(label)
+          group.number += 1 // 每选中一个就 +1
+
+          group.attachmentList.push({ name: attach.name })
+        }
+      })
+    })
+  })
+
+  // 转为数组
+  summaryList.value = Array.from(map.values())
+}
+
+// 👇 监听 attachmentData 中所有 isSelect 的变化
+watch(
+  () => {
+    // 创建一个扁平化的路径数组,用于监听所有 isSelect
+    return attachmentData.value.map((item) =>
+      item.typeList?.map((type) => type.attachmentList?.map((att) => att.isSelect))
+    )
+  },
+  () => {
+    generateSummary()
+  },
+  { deep: true }
+)
+
+const handleFileDownload = (row: any) => {
+  // 如果from_system的值是TOPOCEAN_KSMART,不需要拼接url
+  const url = row?.url
+  // 创建一个隐藏的 <a> 标签
+  const link = document.createElement('a')
+  link.href = row?.is_topocean ? url : import.meta.env.VITE_API_HOST + '/' + url
+  link.target = '_blank'
+
+  // 指定下载文件名(可选)
+  // link.download = row?.file_name || 'file'
+
+  // 添加到 DOM 中,触发点击事件,然后移除
+  document.body.appendChild(link)
+  link.click()
+  document.body.removeChild(link)
+}
+
+const getFileNameFromContentDisposition = (contentDisposition) => {
+  const filenameStart = contentDisposition.indexOf('filename=')
+  if (filenameStart === -1) return null // 如果没有找到,直接返回
+
+  const substring = contentDisposition.slice(filenameStart + 9) // 9 是 'filename='.length
+
+  const firstQuote = substring.indexOf('"')
+
+  if (firstQuote === -1) return null // 如果没有找到开始引号,直接返回
+
+  const secondQuote = substring.indexOf('"', firstQuote + 1)
+
+  if (secondQuote === -1) return null // 如果没有找到结束引号,直接返回
+
+  return substring.slice(firstQuote + 1, secondQuote)
+}
+const handleDownloadAllSelectedFiles = (label?: string) => {
+  const selectedFiles = []
+  attachmentData.value.forEach((item) => {
+    item?.typeList?.forEach((type) => {
+      // 如果选择了 label,则只下载该类型的附件
+      if (label && type.label !== label) return
+      type?.attachmentList?.forEach((attach) => {
+        if (attach.isSelect) {
+          selectedFiles.push(attach)
+        }
+      })
+    })
+  })
+  if (selectedFiles.length === 0) {
+    ElMessage.warning('Please select at least one file to download.')
+    return
+  }
+  bodyLoading.value = true
+
+  $api
+    .downloadAttachment({
+      data: selectedFiles
+    })
+    .then((res: any) => {
+      if (res.status !== 200) {
+        ElMessageBox.alert('The request failed. Please try again later', 'Prompt', {
+          confirmButtonText: 'OK',
+          confirmButtonClass: 'el-button--dark'
+        })
+        return
+      }
+      if (res.data?.code === 403) {
+        sessionStorage.clear()
+        emitter.emit('login-out')
+        router.push('/login')
+        userStore.logout()
+        ElMessage.warning({
+          message: 'Please log in to use this feature.',
+          grouping: true
+        })
+        return
+      } else if (res.data?.code === 500) {
+        ElMessageBox.alert(res.data.msg, 'Prompt', {
+          confirmButtonText: 'OK',
+          confirmButtonClass: 'el-button--dark'
+        })
+        return
+      }
+      const fileName = getFileNameFromContentDisposition(res.headers['content-disposition'])
+      const blob = new Blob([res.data], { type: 'application/zip' })
+      const downloadUrl = window.URL.createObjectURL(blob)
+      const a = document.createElement('a')
+      a.download = fileName
+      a.href = downloadUrl
+      document.body.appendChild(a)
+      a.click()
+      window.URL.revokeObjectURL(downloadUrl)
+      document.body.removeChild(a)
+    })
+    .finally(() => {
+      bodyLoading.value = false
+    })
+}
+</script>
+
+<template>
+  <div
+    class="tracking-download-attachment"
+    v-loading.fullscreen.lock="bodyLoading"
+    element-loading-text="Loading..."
+    element-loading-custom-class="element-loading"
+    element-loading-background="rgb(43, 47, 54, 0.7)"
+    v-vloading="pageLoading"
+  >
+    <div class="left-select-section">
+      <div class="header-select-all">
+        <el-checkbox v-model="isAllSelected"><span>Select All</span></el-checkbox>
+      </div>
+      <div class="attachment-list">
+        <div class="attachment-item" v-for="attItem in attachmentData" :key="attItem.id">
+          <div class="top-number">
+            <el-checkbox
+              :disabled="!attItem?.typeList?.length"
+              @change="handleParentToggle(attItem)"
+              v-model="attItem.isSelect"
+            >
+              <span class="font_family icon-icon_ocean_b"></span>
+              <el-tooltip effect="dark" :content="`Attachment ${attItem.no}`" placement="top">
+                <span class="label ellipsis-text">Attachment {{ attItem.no }}</span>
+              </el-tooltip>
+            </el-checkbox>
+          </div>
+          <div class="attachment-content">
+            <div
+              class="attachment-type"
+              v-for="typeItem in attItem?.typeList"
+              :key="typeItem.label"
+            >
+              <div class="type-label">
+                {{ typeItem.label }} ({{ typeItem.attachmentList.length }})
+              </div>
+              <div class="type-attachment-list">
+                <div
+                  class="attachment-file"
+                  v-for="fileItem in typeItem.attachmentList"
+                  :key="fileItem.name"
+                >
+                  <el-checkbox v-model="fileItem.isSelect" @change="handleChildToggle(attItem)">
+                    <span>{{ fileItem.name }}</span></el-checkbox
+                  >
+                  <span
+                    @click="handleFileDownload(fileItem)"
+                    class="font_family icon-icon_download_b"
+                  ></span>
+                </div>
+              </div>
+            </div>
+            <div class="empty-attachment" v-if="!attItem?.typeList?.length">no file</div>
+          </div>
+        </div>
+      </div>
+    </div>
+    <div class="right-summary-section">
+      <div class="title">Attachment Summary</div>
+      <div class="summary-content">
+        <el-button
+          class="el-button--main el-button--pain-theme"
+          style="width: 100%; margin-bottom: 8px"
+          @click="handleDownloadAllSelectedFiles()"
+        >
+          <span class="font_family icon-icon_download_b"></span>
+          <span>Download Selected ({{ allChooseFiles }})</span>
+        </el-button>
+        <el-collapse
+          style="margin: 8px 0"
+          expand-icon-position="left"
+          v-for="(typeItem, index) in summaryList"
+          :key="index"
+        >
+          <div class="right-download">
+            <div class="count" v-if="typeItem?.attachmentList?.length">
+              <span>{{ typeItem?.attachmentList?.length }}</span>
+            </div>
+            <span
+              @click="handleDownloadAllSelectedFiles(typeItem.label)"
+              class="font_family icon-icon_download_b"
+            ></span>
+          </div>
+          <el-collapse-item :title="typeItem.label" :name="index.toString()">
+            <template #icon="{ isActive }">
+              <span
+                :class="{ 'is-active': isActive }"
+                class="font_family icon-icon_up_b custom-arrow"
+              ></span>
+            </template>
+
+            <div class="attachment-list">
+              <div
+                class="attachment-item"
+                v-for="attItem in typeItem?.attachmentList"
+                :key="attItem.name"
+              >
+                <v-ellipsis-tooltip
+                  :max-width="276"
+                  :max-height="32"
+                  :line-clamp="1"
+                  :content="attItem.name"
+                ></v-ellipsis-tooltip>
+              </div>
+            </div>
+          </el-collapse-item>
+        </el-collapse>
+        <div class="empty-file-data" v-if="!summaryList?.length">
+          <img src="./images/empty-img.png" alt="empty-data" />
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.tracking-download-attachment {
+  display: flex;
+  height: 100%;
+  padding-left: 24px;
+  .left-select-section {
+    flex: 1;
+    .header-select-all {
+      :deep(.el-checkbox__inner) {
+        &::after {
+          top: 1px;
+          left: 7px;
+          height: 14px;
+          width: 6px;
+          border-width: 2.5px;
+        }
+      }
+    }
+  }
+  :deep(.el-checkbox__inner) {
+    &::after {
+      top: 1px;
+      left: 4px;
+      height: 9px;
+      width: 4px;
+      border-width: 2px;
+    }
+  }
+  .right-summary-section {
+    width: 340px;
+    height: 100%;
+    border: 1px solid var(--color-border);
+    min-height: 400px;
+    background-color: var(--color-attchment-summary-bg);
+    .empty-file-data {
+      padding-top: 68px;
+      text-align: center;
+    }
+  }
+}
+.left-select-section {
+  height: 100%;
+  overflow: auto;
+  .header-select-all {
+    margin: 16px 0;
+    span {
+      font-size: 18px;
+      font-weight: 700;
+    }
+    :deep(.el-checkbox__inner) {
+      width: 24px;
+      height: 24px;
+    }
+  }
+  & > .attachment-list {
+    display: grid;
+    grid-template-columns: repeat(auto-fill, minmax(326px, 1fr));
+    grid-template-rows: 320px;
+    gap: 8px;
+    padding-bottom: 36px;
+    padding-right: 24px;
+    height: calc(100% - 64px);
+    overflow: auto;
+    :deep(.el-checkbox__label) {
+      display: flex;
+      align-items: center;
+      & > .label {
+        margin-top: 3px;
+      }
+    }
+  }
+}
+.left-select-section .attachment-list .attachment-item {
+  height: 320px;
+  border: 1px solid var(--color-border);
+  border-radius: 12px;
+  overflow: hidden;
+
+  .top-number {
+    display: flex;
+    align-items: center;
+    height: 48px;
+    padding: 13px 8px;
+    background-color: var(--color-dialog-header-bg);
+    :deep(.el-checkbox) {
+      width: 100%;
+      .el-checkbox__label {
+        width: calc(100% - 8px);
+      }
+    }
+
+    .font_family {
+      font-size: 24px;
+      margin-right: 8px;
+    }
+    .label {
+      font-size: 18px;
+    }
+    .ellipsis-text {
+      width: calc(100% - 50px);
+      display: block;
+      overflow: hidden;
+      text-overflow: ellipsis;
+      white-space: nowrap;
+    }
+    :deep(.el-checkbox__inner) {
+      width: 16px;
+      height: 16px;
+    }
+  }
+  .attachment-content {
+    padding: 13px 8px;
+    overflow: auto;
+    height: calc(100% - 48px);
+    .empty-attachment {
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      height: 100%;
+      color: var(--color-neutral-2);
+    }
+  }
+  .attachment-type {
+    margin-bottom: 8px;
+    .type-label {
+      margin: 5px 0;
+      font-size: 12px;
+      color: var(--color-neutral-2);
+    }
+    .type-attachment-list {
+      display: flex;
+      flex-direction: column;
+      border-radius: 6px;
+      overflow: hidden;
+
+      .attachment-file {
+        display: flex;
+        align-items: center;
+        justify-content: space-between;
+        height: 40px;
+        padding: 0 8px;
+        background-color: var(--color-personal-preference-bg);
+        &:nth-child(n + 2) {
+          border-top: 1px solid var(--color-border);
+        }
+        :deep(.el-checkbox__inner) {
+          display: flex;
+          align-items: center;
+          justify-content: center;
+          width: 16px;
+          height: 16px;
+        }
+        .icon-icon_file_pdf {
+          color: #e74c3c;
+          margin-right: 8px;
+        }
+      }
+    }
+  }
+}
+.right-summary-section {
+  .title {
+    font-size: 24px;
+    font-weight: 700;
+    padding: 16px 8px;
+    border-bottom: 1px solid var(--color-border);
+  }
+  .summary-content {
+    height: calc(100% - 64px);
+    padding: 16px 8px;
+    padding-bottom: 20px;
+    overflow: auto;
+  }
+
+  .el-collapse {
+    position: relative;
+    padding: 0 8px;
+    background-color: var(--color-mode);
+    border: 1px solid var(--color-border);
+    border-radius: 12px;
+    overflow: hidden;
+    :deep(.el-collapse-item__wrap) {
+      border: none;
+    }
+    :deep(.el-collapse-item__header) {
+      gap: 3px;
+      border: none;
+    }
+    :deep(.el-collapse-item__title) {
+      font-weight: 700;
+    }
+    .right-download {
+      position: absolute;
+      right: 14px;
+      top: 14px;
+      display: flex;
+      align-items: center;
+      gap: 16px;
+    }
+    .count {
+      display: inline-flex;
+      align-items: center;
+      justify-content: center;
+      height: 16px;
+      // padding-top: 1px;
+      padding-left: 5px;
+      padding-right: 4px;
+      min-width: 16px;
+      background-color: var(--color-theme);
+      border-radius: 9px;
+      font-size: 10px;
+      font-weight: 700;
+      line-height: 18px;
+      text-align: center;
+      span {
+        height: 17px;
+        color: var(--color-white);
+        font-weight: 700;
+      }
+    }
+    .custom-arrow {
+      transform: rotate(90deg);
+      transition: transform 0.3s ease;
+      transform: rotate(90deg);
+    }
+
+    .custom-arrow.is-active {
+      transform: rotate(180deg);
+    }
+  }
+  .attachment-list {
+    margin-bottom: 8px;
+    border-radius: 8px;
+    overflow: hidden;
+    .attachment-item {
+      height: 32px;
+      padding: 7px 8px 0;
+      border-bottom: 1px solid var(--color-border);
+      background-color: var(--color-personal-preference-bg);
+      &:last-child {
+        border-bottom: none;
+      }
+      span {
+        color: var(--color-neutral-2);
+      }
+    }
+  }
+}
+</style>

BIN
src/views/Tracking/src/components/DownloadAttachment/src/images/empty-img.png


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

@@ -140,7 +140,6 @@ const SubscribeShipments = () => {
     })
     .then((res: any) => {
       if (res.code === 200) {
-        console.log(res.data)
       }
     })
 }

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

@@ -97,7 +97,7 @@ const handleDownload = (row: any) => {
   link.click()
   document.body.removeChild(link)
 }
-const handleDelete = (row: any) => {}
+
 const uploadFilesRef = ref<InstanceType<typeof UploadFilesDialog> | null>(null)
 
 const openUploadFilesDialog = () => {

+ 4 - 4
src/views/Tracking/src/components/TrackingGuide.vue

@@ -234,14 +234,14 @@ defineExpose({
 
 <style lang="scss" scoped>
 .download-file-guide-class {
-  right: 187px;
+  right: 183px;
   top: 246px;
-  width: 431px;
+  width: 692px;
   height: 304px;
   transform: translate(0.7px, -0.3px);
   &.download-file-guide-dark-class {
-    right: 187px;
-    width: 431px;
+    right: 183px;
+    width: 695px;
     height: 304px;
   }
 }

+ 128 - 12
src/views/Tracking/src/components/TrackingTable/src/TrackingTable.vue

@@ -10,9 +10,11 @@ import { useLoadingState } from '@/stores/modules/loadingState'
 import { useThemeStore } from '@/stores/modules/theme'
 import { useVisitedRowState } from '@/stores/modules/visitedRow'
 import { formatTimezone, formatNumber } from '@/utils/tools'
+import { useTrackingDownloadData } from '@/stores/modules/trackingDownloadData'
 
 const visitedRowState = useVisitedRowState()
 const themeStore = useThemeStore()
+const trackingDownloadData = useTrackingDownloadData()
 
 const router = useRouter()
 const props = defineProps({
@@ -375,6 +377,7 @@ onMounted(() => {
   tableRef.value && autoWidth(trackingTable.value, tableRef.value)
 })
 
+const upIcon = ref(false)
 const downloadDialogRef = ref()
 const handleDownload = () => {
   const curSelectedColumns: string[] = []
@@ -389,6 +392,30 @@ const handleDownload = () => {
     selectedNumber.value || pageInfo.value.total
   )
 }
+const handleDownloadAttachments = () => {
+  const serial_no_arr: string[] = []
+  const schemas_arr: string[] = []
+  // 将选中的记录的 serial_no 和 _schemas 收集到数组中
+  const selectedRecords = tableRef.value.getCheckboxRecords()
+
+  if (selectedRecords.length === 0) {
+    ElMessageBox.alert('Please select at least one record to download attachments', {
+      confirmButtonText: 'OK',
+      confirmButtonClass: 'el-button--dark',
+      customClass: 'tracking-table-download-alert-popup'
+    })
+    return
+  }
+  selectedRecords.forEach((item: any) => {
+    serial_no_arr.push(item.serial_no)
+    schemas_arr.push(item._schemas)
+  })
+  trackingDownloadData.setData(serial_no_arr, schemas_arr)
+
+  router.push({
+    name: 'Tracking Download Attachment'
+  })
+}
 
 const exportLoading = ref(false)
 // 获取导出表格数据
@@ -483,7 +510,11 @@ const handleCustomizeColumns = () => {
       model_name: 'Ocean_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 shipment list'
+  )
 }
 
 // 定制表格
@@ -574,7 +605,6 @@ const SubscribeShipments = (row: any) => {
     })
     .then((res: any) => {
       if (res.code === 200) {
-        console.log(res.data)
       }
     })
 }
@@ -599,17 +629,44 @@ defineExpose({
     <div class="table-tools">
       <div class="left-total-records">{{ selectedNumber }} Selected</div>
       <div class="right-tools-btn">
-        <el-button
-          class="el-button--main el-button--pain-theme"
-          @click="handleDownload"
-          :style="{
-            paddingRight: themeStore.theme === 'dark' ? '13px' : '16px',
-            paddingLeft: themeStore.theme === 'dark' ? '13px' : '11px'
-          }"
+        <el-popover
+          trigger="click"
+          top="15vh"
+          class="box-item"
+          :width="226"
+          placement="bottom-start"
         >
-          <span style="margin-right: 7px" class="font_family icon-icon_download_b"></span>
-          Download
-        </el-button>
+          <template #reference>
+            <el-button
+              class="el-button--main el-button--pain-theme download-btn"
+              @click="upIcon = !upIcon"
+              :style="{
+                paddingRight: themeStore.theme === 'dark' ? '13px' : '16px',
+                paddingLeft: themeStore.theme === 'dark' ? '13px' : '11px'
+              }"
+            >
+              <span style="margin-right: 7px" class="font_family icon-icon_download_b"></span>
+              Download
+              <span
+                class="font_family icon-icon_up_b download-up-icon"
+                :class="{ 'rotate-icon': upIcon }"
+              ></span>
+            </el-button>
+          </template>
+          <template #default>
+            <div style="width: 226px; padding: 8px">
+              <div class="download-option-item" @click="handleDownload">
+                <span class="font_family icon-icon_download_b"></span>
+                <span>Download Shipment Details</span>
+              </div>
+              <div class="download-option-item" @click="handleDownloadAttachments">
+                <span class="font_family icon-icon_download__template_b"></span>
+                <span>Download Attachments</span>
+              </div>
+            </div>
+          </template>
+        </el-popover>
+
         <el-button style="padding-left: 10px" type="default" @click="handleCustomizeColumns">
           <span style="margin-right: 6px" class="font_family icon-icon_column_b"></span>
           Customize Columns
@@ -716,6 +773,29 @@ defineExpose({
 </template>
 
 <style lang="scss" scoped>
+.download-option-item {
+  display: flex;
+  align-items: center;
+  padding: 6px 8px;
+  border-radius: 4px;
+  cursor: pointer;
+  &:hover {
+    background-color: var(--color-btn-action-bg-hover);
+    span {
+      color: var(--color-theme);
+    }
+  }
+  span {
+    &:first-child {
+      font-size: 16px;
+      margin-right: 4px;
+      line-height: 18px;
+    }
+    line-height: 22px;
+    color: var(--color-text-secondary);
+  }
+}
+
 .table-tools {
   position: relative;
   display: flex;
@@ -728,6 +808,23 @@ defineExpose({
     font-weight: 700;
     line-height: 32px;
   }
+  .right-tools-btn {
+    // .download-btn {
+    //   &:hover {
+    //     .download-up-icon {
+    //       transform: rotate(360deg);
+    //     }
+    //   }
+    // }
+    .download-up-icon {
+      margin-left: 2px;
+      transition: all 0.5s ease;
+      transform: rotate(180deg);
+      &.rotate-icon {
+        transform: rotate(0deg);
+      }
+    }
+  }
 }
 
 .bottom-pagination {
@@ -782,3 +879,22 @@ defineExpose({
   }
 }
 </style>
+<style lang="scss">
+.tracking-table-download-alert-popup {
+  width: 400px;
+
+  .el-message-box__header {
+    display: none;
+  }
+  .el-message-box__content {
+    padding-top: 9px;
+  }
+  div.el-message-box__btns {
+    border: none;
+    .el-button--dark {
+      width: 100px;
+      height: 40px;
+    }
+  }
+}
+</style>

BIN
src/views/Tracking/src/image/dark-download-guide.png


BIN
src/views/Tracking/src/image/download-guide.png


+ 1 - 0
src/views/Video/index.ts

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

+ 21 - 0
src/views/Video/src/VideoView.vue

@@ -0,0 +1,21 @@
+<script setup lang="ts"></script>
+
+<template>
+  <div class="video-view">
+    <video controls style="width: calc(100% - 20px); height: 100%">
+      <source src="/videos/demo-video.mp4" type="video/mp4" />
+    </video>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.video-view {
+  width: 100%;
+  height: 100%;
+  padding: 25px;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  background-color: #000;
+}
+</style>

この差分においてかなりの量のファイルが変更されているため、一部のファイルを表示していません