Prechádzať zdrojové kódy

Merge branch 'master_zyh' of United_Software/k_online_ui into master

Jack Zhou 1 týždeň pred
rodič
commit
264fd5bab2
70 zmenil súbory, kde vykonal 7135 pridanie a 391 odobranie
  1. 3 1
      src/api/index.ts
  2. 282 0
      src/api/module/report.ts
  3. 4 4
      src/components/AIRobot/src/AIRobot.vue
  4. 2 4
      src/components/AutoComplete/src/AutoComplete.vue
  5. 109 130
      src/components/CreateAddRules/src/CreateAddRules.vue
  6. 3 0
      src/components/CustomizeColumns/src/CustomizeColumns.vue
  7. 1 2
      src/components/SelectTableSelect/src/SelectTableSelect.vue
  8. 35 0
      src/components/TableImgEmpty/TableImgEmpty.vue
  9. BIN
      src/components/TableImgEmpty/image/empty-dark.png
  10. BIN
      src/components/TableImgEmpty/image/empty-light.png
  11. 0 0
      src/components/TableImgEmpty/index.ts
  12. 44 44
      src/components/VBreadcrumb/src/VBreadcrumb.vue
  13. 19 1
      src/components/VTag/src/VTag.vue
  14. 1 2
      src/components/selectAutoSelect/src/selectAutoSelect.vue
  15. 8 8
      src/hooks/calculatingHeight.ts
  16. 59 18
      src/router/index.ts
  17. 33 80
      src/stores/modules/breadCrumb.ts
  18. 65 4
      src/styles/Antdui.scss
  19. 16 6
      src/styles/elementui.scss
  20. 60 4
      src/styles/icons/iconfont.css
  21. 0 0
      src/styles/icons/iconfont.js
  22. 0 0
      src/styles/icons/iconfont.svg
  23. BIN
      src/styles/icons/iconfont.ttf
  24. BIN
      src/styles/icons/iconfont.woff
  25. BIN
      src/styles/icons/iconfont.woff2
  26. 4 0
      src/styles/theme-g.scss
  27. 14 1
      src/styles/theme.scss
  28. 4 1
      src/styles/vxeTable.scss
  29. 42 18
      src/utils/axios.ts
  30. 14 1
      src/utils/tools.ts
  31. 1 2
      src/views/AIRobotChat/src/AIRobotChat.vue
  32. 4 2
      src/views/Booking/src/components/BookingDetail/src/components/EmailView.vue
  33. 10 5
      src/views/DestinationDelivery/src/components/ConfiguRations/src/ConfiguRations.vue
  34. 22 17
      src/views/DestinationDelivery/src/components/ConfiguRations/src/components/ConfigurationsTable.vue
  35. 1 1
      src/views/DestinationDelivery/src/components/CreateNewBooking/src/CreateNewbooking.vue
  36. 1 1
      src/views/DestinationDelivery/src/components/TableView/src/TableView.vue
  37. 5 1
      src/views/Layout/src/LayoutView.vue
  38. 1 1
      src/views/Layout/src/components/Header/components/NotificationDrawer.vue
  39. 22 13
      src/views/Layout/src/components/Menu/MenuView.vue
  40. 1 0
      src/views/Report/index.ts
  41. 222 0
      src/views/Report/src/ReportView.vue
  42. 1 0
      src/views/Report/src/components/ReportDetail/index.ts
  43. 259 0
      src/views/Report/src/components/ReportDetail/src/ReportDetail.vue
  44. 321 0
      src/views/Report/src/components/ReportDetail/src/components/FieldsTable.vue
  45. 261 0
      src/views/Report/src/components/ReportDetail/src/components/ManageReportFields.vue
  46. BIN
      src/views/Report/src/components/ReportDetail/src/images/empty-dark.png
  47. BIN
      src/views/Report/src/components/ReportDetail/src/images/empty-light.png
  48. 1 0
      src/views/Report/src/components/ReportSchedule/index.ts
  49. 196 0
      src/views/Report/src/components/ReportSchedule/src/ReportSchedule.vue
  50. 507 0
      src/views/Report/src/components/ReportSchedule/src/components/EmailConfiguration.vue
  51. 246 0
      src/views/Report/src/components/ReportSchedule/src/components/FieldsTable.vue
  52. 261 0
      src/views/Report/src/components/ReportSchedule/src/components/ManageReportFields.vue
  53. 523 0
      src/views/Report/src/components/ReportSchedule/src/components/TimeRange.vue
  54. 178 0
      src/views/Report/src/components/ReportSchedule/src/components/ValidityPeriod.vue
  55. 3 3
      src/views/SystemSettings/src/SystemSettings.vue
  56. 7 7
      src/views/SystemSettings/src/components/MonitoringTable/src/MonitoringTable.vue
  57. 1 1
      src/views/SystemSettings/src/components/SettingTable/src/SettingTable.vue
  58. 1 0
      src/views/TemplateManagement/index.ts
  59. 288 0
      src/views/TemplateManagement/src/TemplateManagement.vue
  60. 1 0
      src/views/TemplateManagement/src/components/CreateReportTemplate/index.ts
  61. 471 0
      src/views/TemplateManagement/src/components/CreateReportTemplate/src/CreateReportTemplate.vue
  62. 146 0
      src/views/TemplateManagement/src/components/CreateReportTemplate/src/components/AccountSelect.vue
  63. 796 0
      src/views/TemplateManagement/src/components/CreateReportTemplate/src/components/AdjustmentField.vue
  64. 185 0
      src/views/TemplateManagement/src/components/CreateReportTemplate/src/components/GroupNameSelect.vue
  65. 145 0
      src/views/TemplateManagement/src/components/CreateReportTemplate/src/components/PartyIDSelect.vue
  66. 733 0
      src/views/TemplateManagement/src/components/CreateReportTemplate/src/components/ReportFieldsConfiguration.vue
  67. 1 0
      src/views/TemplateManagement/src/components/TableView/index.ts
  68. 481 0
      src/views/TemplateManagement/src/components/TableView/src/TableView.vue
  69. 4 2
      src/views/Tracking/src/components/TrackingDetail/src/components/EmailDrawer.vue
  70. 6 6
      src/views/Tracking/src/components/TrackingDetail/src/components/UploadFilesDialog.vue

+ 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 // 取出所有可遍历属性赋值在新的对象上

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

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

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

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

+ 109 - 130
src/components/CreateAddRules/src/CreateAddRules.vue

@@ -159,7 +159,8 @@ const Initdata = () => {
             FrequencyDataCon.value = res.data.Container_Status_Update
             MethodsDataCon.value = res.data.Container_Status_Update
             ShipmentRangeCon.value = res.data.Container_Status_Update
-            createObj.Transportstr = res.data.Container_Status_Update.shipment_details.split(';\r\n')[0]
+            createObj.Transportstr =
+              res.data.Container_Status_Update.shipment_details.split(';\r\n')[0]
             createObj.Timestr = res.data.Container_Status_Update.shipment_details.split(';\r\n')[1]
             createListContainer.value.push(createObj.Transportstr)
             createListContainer.value.push(createObj.Timestr)
@@ -179,8 +180,10 @@ const Initdata = () => {
             FrequencyDataDep.value = res.data['Departure/Arrival_Delay']
             MethodsDataDep.value = res.data['Departure/Arrival_Delay']
             ShipmentRangeDep.value = res.data['Departure/Arrival_Delay']
-            createObj.Transportstr = res.data['Departure/Arrival_Delay'].shipment_details.split(';\r\n')[0]
-            createObj.Timestr = res.data['Departure/Arrival_Delay'].shipment_details.split(';\r\n')[1]
+            createObj.Transportstr =
+              res.data['Departure/Arrival_Delay'].shipment_details.split(';\r\n')[0]
+            createObj.Timestr =
+              res.data['Departure/Arrival_Delay'].shipment_details.split(';\r\n')[1]
             createListDeparture.value.push(createObj.Transportstr)
             createListDeparture.value.push(createObj.Timestr)
           } else if (editTablerules_type == 'ETD/ETA_Change') {
@@ -281,7 +284,7 @@ const changecheckCreateRulesMilestone = (val: any, value: any) => {
   savesubscribeobj.shipment_transport_mode = value
   savesubscribeobj.shipment_details = createListMilestone.value.join(';\n')
 }
-const ChangeCheckTimeRulesMilestone = (val: any, time: any, timeend :any) => {
+const ChangeCheckTimeRulesMilestone = (val: any, time: any, timeend: any) => {
   createListMilestone.value = []
   createObj.Timestr = val
   if (val.includes('ETD')) {
@@ -316,7 +319,7 @@ const changecheckCreateRulesContainer = (val: any, value: any) => {
   savesubscribeobj.shipment_transport_mode = value
   savesubscribeobj.shipment_details = createListContainer.value.join(';\n')
 }
-const ChangeCheckTimeRulesContainer = (val: any, time: any, timeend :any) => {
+const ChangeCheckTimeRulesContainer = (val: any, time: any, timeend: any) => {
   createListContainer.value = []
   createObj.Timestr = val
   if (val.includes('ETD')) {
@@ -351,7 +354,7 @@ const changecheckCreateRulesDeparture = (val: any, value: any) => {
   savesubscribeobj.shipment_transport_mode = value
   savesubscribeobj.shipment_details = createListDeparture.value.join(';\n')
 }
-const ChangeCheckTimeRulesDeparture = (val: any, time: any, timeend :any) => {
+const ChangeCheckTimeRulesDeparture = (val: any, time: any, timeend: any) => {
   createListDeparture.value = []
   createObj.Timestr = val
   if (val.includes('ETD')) {
@@ -386,7 +389,7 @@ const changecheckCreateRulesETDChange = (val: any, value: any) => {
   savesubscribeobj.shipment_transport_mode = value
   savesubscribeobj.shipment_details = createListETDChange.value.join(';\n')
 }
-const ChangeCheckTimeRulesETDChange = (val: any, time: any, timeend :any) => {
+const ChangeCheckTimeRulesETDChange = (val: any, time: any, timeend: any) => {
   createListETDChange.value = []
   createObj.Timestr = val
   if (val.includes('ETD')) {
@@ -578,20 +581,19 @@ const SaveSuceessful = () => {
     })
     .then((res: any) => {
       if (res.code === 200) {
-        if(res.data.msg == 'Update Successful') {
+        if (res.data.msg == 'Update Successful') {
           SaveedVisible.value = true
           setTimeout(() => {
             SaveedVisible.value = false
-            sessionStorage.setItem('activeTab', 'Monitoring Settings')
+            sessionStorage.setItem('activeTab', 'System Settings')
             router.push({
-              path: '/SystemSettings',
+              path: '/system-settings',
               query: {}
             })
           }, 3000)
-        } else if(res.data.msg == 'Similar Rule Detected') {
+        } else if (res.data.msg == 'Similar Rule Detected') {
           SaveVisibleDetected.value = true
-        } else if(res.data.msg == 'Unable to Save')
-        SaveVisibleError.value = true
+        } else if (res.data.msg == 'Unable to Save') SaveVisibleError.value = true
       }
     })
 }
@@ -606,15 +608,15 @@ const HandelSaveVisibleDetected = () => {
     })
     .then((res: any) => {
       if (res.code === 200) {
-          SaveedVisible.value = true
-          setTimeout(() => {
-            SaveedVisible.value = false
-            sessionStorage.setItem('activeTab', 'Monitoring Settings')
-            router.push({
-              path: '/SystemSettings',
-              query: {}
-            })
-          }, 3000)
+        SaveedVisible.value = true
+        setTimeout(() => {
+          SaveedVisible.value = false
+          sessionStorage.setItem('activeTab', 'System Settings')
+          router.push({
+            path: '/system-settings',
+            query: {}
+          })
+        }, 3000)
       }
     })
 }
@@ -624,8 +626,7 @@ const Savesubscribe = () => {
   if (props.TitleType == 'Milestone') {
     savesubscribeobj.rules_type = 'Milestone_Update'
     if (
-      OceanCheckList.value.length == 0 &&
-      AirCheckList.value.length == 0 ||
+      (OceanCheckList.value.length == 0 && AirCheckList.value.length == 0) ||
       MilFrequencyList.value.length == 0 ||
       MilMethodsList.value.length == 0 ||
       createObj.Transportstr == '' ||
@@ -637,7 +638,7 @@ const Savesubscribe = () => {
       if (createObj.Timestr == '') {
         missingmessage.value += 'Time, '
       }
-      if (OceanCheckList.value.length == 0 && AirCheckList.value.length == 0 ) {
+      if (OceanCheckList.value.length == 0 && AirCheckList.value.length == 0) {
         missingmessage.value += 'Select Milestone, '
       }
       if (MilFrequencyList.value.length == 0) {
@@ -651,21 +652,17 @@ const Savesubscribe = () => {
     } else {
       savesubscribeobj.ocean_milestone = OceanCheckListCode.value
       savesubscribeobj.air_milestone = AirCheckListCode.value
-      if(OceanCheckList.value.length == 0) {
-        str =
-        'Air Milestones: ' +
-        AirCheckList.value.join(',')
-      } else if(AirCheckList.value.length == 0) {
-        str =
-        'Ocean Milestones: ' +
-        OceanCheckList.value.join(',')
+      if (OceanCheckList.value.length == 0) {
+        str = 'Air Milestones: ' + AirCheckList.value.join(',')
+      } else if (AirCheckList.value.length == 0) {
+        str = 'Ocean Milestones: ' + OceanCheckList.value.join(',')
       } else {
         str =
-        'Ocean Milestones: ' +
-        OceanCheckList.value.join(',') +
-        ';\nAir Milestones: ' +
-        AirCheckList.value.join(',') +
-        ';'
+          'Ocean Milestones: ' +
+          OceanCheckList.value.join(',') +
+          ';\nAir Milestones: ' +
+          AirCheckList.value.join(',') +
+          ';'
       }
       savesubscribeobj.event_details = str
       SaveSuceessful()
@@ -704,9 +701,8 @@ const Savesubscribe = () => {
     }
   } else if (props.TitleType == 'Departure') {
     savesubscribeobj.rules_type = 'Departure/Arrival_Delay'
-    if ( 
-      DelayedDeparturedList.value.length == 0 &&
-      DelayedAirdList.value.length == 0 ||
+    if (
+      (DelayedDeparturedList.value.length == 0 && DelayedAirdList.value.length == 0) ||
       DepFrequencyList.value.length == 0 ||
       DepMethodsList.value == undefined ||
       DepMethodsList.value.length == 0 ||
@@ -731,21 +727,17 @@ const Savesubscribe = () => {
       missingmessage.value = missingmessage.value.substring(0, missingmessage.value.length - 2)
       UnableSaveVisible.value = true
     } else {
-      if(DelayedDeparturedList.value.length == 0) {
-        str =
-        'Air: ' +
-        DelayedAirdList.value.join(',')
-      } else if(DelayedAirdList.value.length == 0) {
-        str =
-        'Ocean: ' +
-        DelayedDeparturedList.value.join(',')
+      if (DelayedDeparturedList.value.length == 0) {
+        str = 'Air: ' + DelayedAirdList.value.join(',')
+      } else if (DelayedAirdList.value.length == 0) {
+        str = 'Ocean: ' + DelayedDeparturedList.value.join(',')
       } else {
         str =
-        'Ocean: ' +
-        DelayedDeparturedList.value.join(',') +
-        ';\nAir: ' +
-        DelayedAirdList.value.join(',') +
-        ';'
+          'Ocean: ' +
+          DelayedDeparturedList.value.join(',') +
+          ';\nAir: ' +
+          DelayedAirdList.value.join(',') +
+          ';'
       }
       savesubscribeobj.event_details = str
       SaveSuceessful()
@@ -753,8 +745,7 @@ const Savesubscribe = () => {
   } else {
     savesubscribeobj.rules_type = 'ETD/ETA_Change'
     if (
-      ETDOceanList.value.length == 0 &&
-      ETDAirList.value.length == 0 ||
+      (ETDOceanList.value.length == 0 && ETDAirList.value.length == 0) ||
       ETDFrequencyList.value.length == 0 ||
       ETDMethodsList.value == undefined ||
       ETDMethodsList.value.length == 0 ||
@@ -779,21 +770,13 @@ const Savesubscribe = () => {
       missingmessage.value = missingmessage.value.substring(0, missingmessage.value.length - 2)
       UnableSaveVisible.value = true
     } else {
-      if(ETDOceanList.value.length == 0) {
-        str =
-        '[Air]' +
-        ETDAirList.value.join(',')
-      } else if(ETDAirList.value.length == 0) {
-        str =
-        '[Ocean]' +
-        ETDOceanList.value.join(',')
+      if (ETDOceanList.value.length == 0) {
+        str = '[Air]' + ETDAirList.value.join(',')
+      } else if (ETDAirList.value.length == 0) {
+        str = '[Ocean]' + ETDOceanList.value.join(',')
       } else {
         str =
-        '[Ocean]' +
-        ETDOceanList.value.join(',') +
-        ';\n[Air]' +
-        ETDAirList.value.join(',') +
-        ';'
+          '[Ocean]' + ETDOceanList.value.join(',') + ';\n[Air]' + ETDAirList.value.join(',') + ';'
       }
       savesubscribeobj.event_details = str
       SaveSuceessful()
@@ -836,14 +819,14 @@ const clearData = (val: any) => {
 const MilOceanref = ref()
 const MilAirref = ref()
 const ContainerOcean = ref()
-const handleCloseMilestoneOcean = (val:any) => {
+const handleCloseMilestoneOcean = (val: any) => {
   MilOceanref.value.hadleclose(val)
 }
-const handleCloseMilestoneAir = (val:any) => {
+const handleCloseMilestoneAir = (val: any) => {
   MilAirref.value.hadleclose(val)
 }
 
-const handleCloseContainer = (val:any) => {
+const handleCloseContainer = (val: any) => {
   ContainerOcean.value.hadleclose(val)
 }
 
@@ -1269,68 +1252,64 @@ defineExpose({
   </el-dialog>
   <!-- 保存成功 -->
   <el-dialog v-model="SaveedVisible" width="320" style="height: 212px">
-    <div style="text-align: center"><el-image :src="submitsucessful" style="width: 64px;" /></div>
+    <div style="text-align: center"><el-image :src="submitsucessful" style="width: 64px" /></div>
     <div style="text-align: center; margin-top: 20px">Saved successfully</div>
   </el-dialog>
   <!-- 保存失败 -->
   <el-dialog v-model="SaveVisibleError" width="480">
-      <div>Duplicate Rule Error.</div>
-      <div>This rule exactly matches an existing rule.</div>
-      <template #footer>
-        <div class="dialog-footer">
-          <el-button
-            class="el-button--danger"
-            @click="SaveVisibleError = false"
-            style="width: 100px"
-          >
-            OK
-          </el-button>
-        </div>
-      </template>
-      <template #header>
-        <div class="cancel_header">
-          <span class="iconfont_icon iconfont_warning">
-            <svg class="iconfont icon_danger" aria-hidden="true">
-              <use xlink:href="#icon-icon_fail_fill_b"></use>
-            </svg>
-          </span>
-          Unable to Save
-        </div>
-      </template>
-    </el-dialog>
-    <!-- 三项重合提示 -->
+    <div>Duplicate Rule Error.</div>
+    <div>This rule exactly matches an existing rule.</div>
+    <template #footer>
+      <div class="dialog-footer">
+        <el-button class="el-button--danger" @click="SaveVisibleError = false" style="width: 100px">
+          OK
+        </el-button>
+      </div>
+    </template>
+    <template #header>
+      <div class="cancel_header">
+        <span class="iconfont_icon iconfont_warning">
+          <svg class="iconfont icon_danger" aria-hidden="true">
+            <use xlink:href="#icon-icon_fail_fill_b"></use>
+          </svg>
+        </span>
+        Unable to Save
+      </div>
+    </template>
+  </el-dialog>
+  <!-- 三项重合提示 -->
   <el-dialog v-model="SaveVisibleDetected" width="480">
-      <div>A similar configuration rule already exists.</div>
-      <div>Would you like to proceed with creating this rule?</div>
-      <template #footer>
-        <div class="dialog-footer">
-          <el-button
-            class="el-button--default"
-            @click="SaveVisibleDetected = false"
-            style="width: 100px"
-          >
-            Cancel
-          </el-button>
-          <el-button
-            class="el-button--warning"
-            @click="HandelSaveVisibleDetected"
-            style="width: 100px"
-          >
-            Save
-          </el-button>
-        </div>
-      </template>
-      <template #header>
-        <div class="cancel_header">
-          <span class="iconfont_icon iconfont_warning">
-            <svg class="iconfont icon_warning" aria-hidden="true">
-              <use xlink:href="#icon-icon_tipsfilled_b"></use>
-            </svg>
-          </span>
-          Similar Rule Detected
-        </div>
-      </template>
-    </el-dialog>
+    <div>A similar configuration rule already exists.</div>
+    <div>Would you like to proceed with creating this rule?</div>
+    <template #footer>
+      <div class="dialog-footer">
+        <el-button
+          class="el-button--default"
+          @click="SaveVisibleDetected = false"
+          style="width: 100px"
+        >
+          Cancel
+        </el-button>
+        <el-button
+          class="el-button--warning"
+          @click="HandelSaveVisibleDetected"
+          style="width: 100px"
+        >
+          Save
+        </el-button>
+      </div>
+    </template>
+    <template #header>
+      <div class="cancel_header">
+        <span class="iconfont_icon iconfont_warning">
+          <svg class="iconfont icon_warning" aria-hidden="true">
+            <use xlink:href="#icon-icon_tipsfilled_b"></use>
+          </svg>
+        </span>
+        Similar Rule Detected
+      </div>
+    </template>
+  </el-dialog>
 </template>
 
 <style lang="scss" scoped>
@@ -1462,4 +1441,4 @@ defineExpose({
 :deep(.el-collapse) {
   margin-right: 8px;
 }
-</style>
+</style>

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

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

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

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

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

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


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


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


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

@@ -17,31 +17,31 @@ const handleGoBack = () => {
 }
 let monitoringQuery = ref()
 const jumpLink = (label: string, query: any) => {
-    if(label == 'Monitoring Settings') {
-      CancelRulesVisible.value = true
-      monitoringQuery.value = query
-    } else if(label == 'Destination Delivery') {
-      router.push({
-        name: 'Destination Delivery',
-        query: query
-      })
-    } else if (label == 'Configurations'){
-      router.push({
-        name: 'Configurations',
-        query: query
-      })
-    }else {
-      label &&
+  if (label == 'System Settings') {
+    CancelRulesVisible.value = true
+    monitoringQuery.value = query
+  } else if (label == 'Destination Delivery') {
+    router.push({
+      name: 'Destination Delivery',
+      query: query
+    })
+  } else if (label == 'Configurations') {
+    router.push({
+      name: 'Configurations',
+      query: query
+    })
+  } else {
+    label &&
       router.push({
         name: label,
         query: query
       })
-    }
+  }
 }
 const jumpLinkMonitoring = () => {
   CancelRulesVisible.value = false
   router.push({
-    name: 'Monitoring Settings',
+    name: 'System Settings',
     query: monitoringQuery.value
   })
 }
@@ -57,39 +57,39 @@ const jumpLinkMonitoring = () => {
     <template v-for="(routeItem, index) in breadCrumb.routeList" :key="routeItem.label">
       <template v-if="index + 1 !== breadCrumb.routeList.length">
         <span @click="jumpLink(routeItem.label, routeItem.query)" class="previous-route">{{
-          routeItem.label
+          routeItem.breadName
         }}</span>
         <span class="interval">|</span>
       </template>
-      <span v-else>{{ routeItem.label }}</span>
+      <span v-else>{{ routeItem.breadName }}</span>
     </template>
   </div>
   <div v-else></div>
-    <!-- 取消保存 -->
-    <el-dialog v-model="CancelRulesVisible" width="480">
-      <div style="font-weight: 400">You have unsaved changes.</div>
-      <div style="font-weight: 400">Are you sure you want to leave this page?</div>
-      <template #footer>
-        <div class="dialog-footer">
-          <el-button type="default" @click="CancelRulesVisible = false" style="width: 100px"
-            >Cancel</el-button
-          >
-          <el-button class="el-button--warning" @click="jumpLinkMonitoring" style="width: 100px">
-            OK
-          </el-button>
-        </div>
-      </template>
-      <template #header>
-        <div class="cancel_header">
-          <span class="iconfont_icon iconfont_warning">
-            <svg class="iconfont icon_warning" aria-hidden="true">
-              <use xlink:href="#icon-icon_tipsfilled_b"></use>
-            </svg>
-          </span>
-          Unsaved Changes
-        </div>
-      </template>
-    </el-dialog>
+  <!-- 取消保存 -->
+  <el-dialog v-model="CancelRulesVisible" width="480">
+    <div style="font-weight: 400">You have unsaved changes.</div>
+    <div style="font-weight: 400">Are you sure you want to leave this page?</div>
+    <template #footer>
+      <div class="dialog-footer">
+        <el-button type="default" @click="CancelRulesVisible = false" style="width: 100px"
+          >Cancel</el-button
+        >
+        <el-button class="el-button--warning" @click="jumpLinkMonitoring" style="width: 100px">
+          OK
+        </el-button>
+      </div>
+    </template>
+    <template #header>
+      <div class="cancel_header">
+        <span class="iconfont_icon iconfont_warning">
+          <svg class="iconfont icon_warning" aria-hidden="true">
+            <use xlink:href="#icon-icon_tipsfilled_b"></use>
+          </svg>
+        </span>
+        Unsaved Changes
+      </div>
+    </template>
+  </el-dialog>
 </template>
 
 <style lang="scss" scoped>

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

+ 59 - 18
src/router/index.ts

@@ -21,6 +21,29 @@ const router = createRouter({
           name: 'Booking',
           component: () => import('../views/Booking')
         },
+        {
+          path: '/report',
+          name: 'Report Management',
+          component: () => import('../views/Report')
+        },
+        {
+          path: '/report/detail',
+          name: 'Report Detail',
+          component: () => import('../views/Report/src/components/ReportDetail'),
+          meta: {
+            activeMenu: '/report',
+            breadName: 'Detail'
+          }
+        },
+        {
+          path: '/report/schedule',
+          name: 'Report Schedule',
+          component: () => import('../views/Report/src/components/ReportSchedule'),
+          meta: {
+            activeMenu: '/report',
+            breadName: 'Schedule Configuration'
+          }
+        },
         {
           path: '/booking/detail',
           name: 'Booking Detail',
@@ -41,7 +64,8 @@ const router = createRouter({
           meta: {
             activeMenu: '/tracking'
           }
-        }, {
+        },
+        {
           path: '/tracking/download-attachment',
           name: 'Tracking Download Attachment',
           component: () => import('../views/Tracking/src/components/DownloadAttachment'),
@@ -99,8 +123,8 @@ const router = createRouter({
           component: () => import('../views/Login/src/components/ChangePasswordCard.vue')
         },
         {
-          path: '/Operationlog',
-          name: 'Operationlog',
+          path: '/operation-log',
+          name: 'Operation log',
           component: () => import('../views/OperationLog')
         },
         {
@@ -114,10 +138,23 @@ const router = createRouter({
           component: () => import('../views/AIApiLog')
         },
         {
-          path: '/PromptConfiguration',
-          name: 'PromptConfiguration',
+          path: '/prompt-configuration',
+          name: 'Prompt Configuration',
           component: () => import('../views/PromptConfiguration')
         },
+        {
+          path: '/template-management',
+          name: 'Template Management',
+          component: () => import('../views/TemplateManagement')
+        },
+        {
+          path: '/template-management/create-report-template',
+          name: 'Create Report Template',
+          component: () => import('../views/TemplateManagement/src/components/CreateReportTemplate'),
+          meta: {
+            activeMenu: '/template-management'
+          }
+        },
         {
           path: '/system-message',
           name: 'System Message',
@@ -133,21 +170,16 @@ const router = createRouter({
           component: () => import('../views/SystemMessage/src/components/SystemMessageDetail.vue')
         },
         {
-          path: '/SystemSettings',
-          name: 'Monitoring Settings',
-          component: () => import('../views/SystemSettings')
-        },
-        {
-          path: '/SystemSettings',
+          path: '/system-settings',
           name: 'System Settings',
           component: () => import('../views/SystemSettings')
         },
         {
-          path: '/SystemSettings/createnewrule',
+          path: '/system-settings/create-new-rule',
           name: 'Create New Rule',
           component: () => import('../views/SystemSettings/src/components/CreateNewrule'),
           meta: {
-            activeMenu: '/SystemSettings'
+            activeMenu: '/system-settings'
           }
         },
         {
@@ -156,19 +188,28 @@ const router = createRouter({
           component: () => import('../views/DestinationDelivery')
         },
         {
-          path: '/destination-delivery/CreateNewBooking',
+          path: '/destination-delivery/create-new-booking',
           name: 'Create New Booking',
           component: () => import('../views/DestinationDelivery/src/components/CreateNewBooking')
         },
         {
-          path: '/destination-delivery/ConfiguRations',
+          path: '/destination-delivery/configurations',
           name: 'Configurations',
-          component: () => import('../views/DestinationDelivery/src/components/ConfiguRations')
+          component: () => import('../views/DestinationDelivery/src/components/ConfiguRations'),
+          meta: {
+            activeMenu: '/destination-delivery'
+          }
         },
         {
-          path: '/destination-delivery/ConfiguRations/CreateNewRule',
+          path: '/destination-delivery/configurations/create-new-rule',
           name: 'Destination Create New Rule',
-          component: () => import('../views/DestinationDelivery/src/components/ConfiguRations/src/components/CreateNewRule.vue')
+          component: () =>
+            import(
+              '../views/DestinationDelivery/src/components/ConfiguRations/src/components/CreateNewRule.vue'
+            ),
+          meta: {
+            activeMenu: '/destination-delivery'
+          }
         },
         {
           path: '/destination-delivery/modify-booking',

+ 33 - 80
src/stores/modules/breadCrumb.ts

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

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

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

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

Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 0 - 0
src/styles/icons/iconfont.js


Rozdielové dáta súboru neboli zobrazené, pretože súbor je príliš veľký
+ 0 - 0
src/styles/icons/iconfont.svg


BIN
src/styles/icons/iconfont.ttf


BIN
src/styles/icons/iconfont.woff


BIN
src/styles/icons/iconfont.woff2


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

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

+ 14 - 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,8 +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 {
@@ -483,6 +491,9 @@
   .el-input {
     --el-border: #656f7d;
   }
+  .el-date-editor {
+    --el-input-border-color: #656f7d;
+  }
   .el-radio {
     --el-radio-input-border: #656f7d;
   }
@@ -585,5 +596,7 @@
   --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

+ 42 - 18
src/utils/axios.ts

@@ -8,23 +8,39 @@ interface codeMessage {
   [key: number]: string
 }
 
-const CODE_MESSAGE: codeMessage = {
-  200: '服务器成功返回请求的数据。',
-  400: '发出的请求有错误,服务器没有进行新建或修改数据的操作。',
-  401: '用户没有权限(令牌、用户名、密码错误)。',
-  403: '用户得到授权,但是访问是被禁止的。',
-  404: '发出的请求针对的是不存在的记录,服务器没有进行操作。',
-  406: '请求的格式不可得。',
-  410: '请求的资源被永久删除,且不会再得到的。',
-  422: '当创建一个对象时,发生一个验证错误。',
-  456: 'refreshToken过期',
-  457: 'accessToken过期',
-  500: '服务器发生错误,请检查服务器。',
-  502: '网关错误。',
-  503: '服务不可用,服务器暂时过载或维护。',
-  504: '网关超时。'
-}
+// const CODE_MESSAGE: codeMessage = {
+//   200: '服务器成功返回请求的数据。',
+//   400: '发出的请求有错误,服务器没有进行新建或修改数据的操作。',
+//   401: '用户没有权限(令牌、用户名、密码错误)。',
+//   403: '用户得到授权,但是访问是被禁止的。',
+//   404: '发出的请求针对的是不存在的记录,服务器没有进行操作。',
+//   406: '请求的格式不可得。',
+//   410: '请求的资源被永久删除,且不会再得到的。',
+//   422: '当创建一个对象时,发生一个验证错误。',
+//   456: 'refreshToken过期',
+//   457: 'accessToken过期',
+//   500: '服务器发生错误,请检查服务器。',
+//   502: '网关错误。',
+//   503: '服务不可用,服务器暂时过载或维护。',
+//   504: '网关超时。'
+// }
 
+const CODE_MESSAGE: codeMessage = {
+  200: 'The server successfully returned the requested data.',
+  400: 'The request was invalid; the server did not create or modify any data.',
+  401: 'Unauthorized: invalid token, username, or password.',
+  403: 'Access is forbidden despite valid authentication.',
+  404: 'The requested resource does not exist; no action was taken by the server.',
+  406: 'The requested format is not available.',
+  410: 'The requested resource has been permanently deleted and will not be available again.',
+  422: 'A validation error occurred while creating an object.',
+  456: 'Refresh token expired.',
+  457: 'Access token expired.',
+  500: 'An internal server error occurred. Please check the server.',
+  502: 'Bad gateway.',
+  503: 'Service unavailable: the server is temporarily overloaded or under maintenance.',
+  504: 'Gateway timeout.'
+};
 class HttpAxios {
   instance: AxiosInstance
   timeout = 30000
@@ -76,24 +92,32 @@ class HttpAxios {
   }
 
   _checkResponseError = (error: any) => {
+    // ✅ 第一步:如果是用户取消的请求,静默处理 or 特殊处理
+    if (axios.isCancel(error)) {
+      return Promise.reject(error) // 通常仍 reject,但上层可选择忽略
+    }
+
+    // ✅ 第二步:超时错误(ECONNABORTED)
     if (error.code === 'ECONNABORTED') {
       ElMessage.error({
-        message: 'Request timed out, please try again later!!',
+        message: 'Request timed out, please try again later!',
         grouping: true
       })
       return Promise.reject(error)
     }
 
+    // ✅ 第三步:其他真实错误
     const status = error.response?.status
     const statusText = error.response?.statusText
     const message = error.message
+
     ElMessage.error({
       message: CODE_MESSAGE[status] || statusText || message,
       grouping: true
     })
+
     return Promise.reject(error)
   }
-
   sendRequest = (url: string, params: any, method = 'post', config?: AxiosRequestConfig) => {
     if (!this.instance) return
 

+ 14 - 1
src/utils/tools.ts

@@ -1,6 +1,8 @@
-import moment from 'moment-timezone'
+import moment from 'moment-timezone';
+import 'moment-timezone/data/packed/latest.json';
 import { useUserStore } from '@/stores/modules/user'
 
+
 const userStore = useUserStore()
 const formatString = computed(() => {
   return userStore.dateFormat || 'MM/DD/YYYY'
@@ -32,6 +34,17 @@ export const formatTimezone = (time: string, timezone?: string, is12HourClock?:
   }
 }
 
+/**
+ * 将服务器时间转换为用户时区时间
+ * @param
+ * @returns
+ */
+export const formatTimezoneByUser = (time: string, timeFormate: string, showHour?: boolean) => {
+  if (!time) return '--'
+  let curFormatString = showHour ? formatString.value + ' HH:mm' : formatString.value
+  return moment.tz(time, timeFormate, 'America/Los_Angeles').local().format(curFormatString)
+}
+
 /**
  * 返回传入地区的UTC时区格式化
  * @param timezone

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

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

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

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

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

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

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

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

+ 1 - 1
src/views/DestinationDelivery/src/components/CreateNewBooking/src/CreateNewbooking.vue

@@ -925,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">

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

@@ -301,7 +301,7 @@ const clickEmailBtn = (row: any) => {
 // edit
 const handleEdit = (row: any) => {
   router.push({
-    path: '/destination-delivery/CreateNewBooking',
+    path: '/destination-delivery/create-new-booking',
     query: { a: row._serial_no }
   })
 }

+ 5 - 1
src/views/Layout/src/LayoutView.vue

@@ -91,7 +91,11 @@ onMounted(() => {
         <router-view />
       </el-main>
     </el-container>
-    <AIRobot @AvatarClick="AvatarClick" @handelClickAIDefault="handelClickAIDefault" @handelclickaiinit=handelclickaiinit></AIRobot>
+    <AIRobot
+      @AvatarClick="AvatarClick"
+      @handelClickAIDefault="handelClickAIDefault"
+      @handelclickaiinit="handelclickaiinit"
+    ></AIRobot>
     <AIRobotChat
       ref="AIRobotChatref"
       v-show="isShowAIRobotChat"

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

@@ -72,7 +72,7 @@ const handleViewAll = () => {
 const handleSettingMessage = () => {
   drawerRef.value.handleClose()
   router.push({
-    name: 'Monitoring Settings',
+    name: 'System Settings',
     query: {
       tab: 'Subscribe Notifications'
     }

+ 22 - 13
src/views/Layout/src/components/Menu/MenuView.vue

@@ -34,7 +34,6 @@ const getMenuList = () => {
   //     index: '2',
   //     label: 'Booking',
   //     icon: 'icon_booking__fill_b',
-  //     // path: '/booking',
   //     type: 'list',
   //     children: [
   //       {
@@ -57,39 +56,50 @@ 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'
+  //         path: '/system-settings'
   //       },
   //       {
-  //         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'
+  //         path: '/operation-log'
   //       },
   //       {
-  //         index: '4-6',
+  //         index: '5-6',
   //         label: 'Prompt Configuration',
-  //         path: '/PromptConfiguration'
+  //         path: '/prompt-configuration'
   //       }
   //     ]
   //   }
@@ -153,8 +163,8 @@ const changeRouter = (path: any) => {
     localStorage.removeItem('loginAI')
     emitter.emit('login-success')
   }
-  if (path == '/PromptConfiguration') {
-    emitter.emit('checkPrompt')
+  if (path == '/prompt-configuration' || path == '/report') {
+    // emitter.emit('checkPrompt')
   } else {
     if (localStorage.getItem('loginAI')) {
       localStorage.removeItem('loginAI')
@@ -193,7 +203,6 @@ const changeRouter = (path: any) => {
 const handleCollapseClick = () => {
   isCollapse.value = !isCollapse.value
 }
-const menuRef = ref()
 
 // 友情链接
 const activeName = ref('1')

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

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

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

@@ -0,0 +1,222 @@
+<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'
+
+const router = useRouter()
+
+const textSearch = ref('')
+// search report name
+const SearchInput = () => {
+  getTableData()
+}
+
+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: 'Report Detail',
+    query: {
+      id: row.serial_no
+    }
+  })
+}
+const handleClickSchedule = (row: any) => {
+  router.push({
+    name: 'Report Schedule',
+    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>
+        <TableImgEmpty />
+      </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;
+}
+</style>

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

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

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

@@ -0,0 +1,259 @@
+<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 reportName = ref('')
+const handleFilterData = (filterData, name) => {
+  reportName.value = name
+  if (filterList.value.length) return
+  filterList.value = filterData.map((item) => {
+    let curData: any = {}
+    if (item.data_type === 'string') {
+      curData.value = ''
+    } else if (item.data_type === 'number' || item.data_type === 'date') {
+      curData.value = []
+    } else if (item.data_type === 'select') {
+      curData.options = item.options
+      curData.value = ''
+    }
+    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>{{ reportName }}</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"
+            clearable
+          ></el-input>
+          <el-select
+            v-else-if="item.type === 'select'"
+            v-model="item.value"
+            clearable
+            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]"
+              clearable
+            ></el-input>
+            -
+            <el-input
+              placeholder="Please enter..."
+              class="no-spinner"
+              type="number"
+              v-model="item.value[1]"
+              clearable
+            ></el-input>
+          </div>
+          <div v-if="item.type === 'date'" style="display: flex; gap: 4px; align-items: center">
+            <el-date-picker
+              v-model="item.value"
+              type="daterange"
+              range-separator="To"
+              start-placeholder="Start date"
+              end-placeholder="End date"
+              style="height: 32px"
+              clearable
+              value-format="MM/DD/YYYY"
+              :format="formatDate"
+            />
+          </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: 16px;
+  }
+  .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>

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

@@ -0,0 +1,321 @@
+<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' || item.type === 'select') {
+      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,
+      sortByField: sortBy.value,
+      sortByOrder: sortOrder.value,
+      ...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
+        tmpMapping = res.data.tmp_mapping
+
+        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, reportName.value)
+      }
+    })
+    .finally(() => {
+      nextTick(() => {
+        tableRef.value && autoWidth(tableData.value, tableRef.value)
+        tableLoadingTable.value = false
+      })
+    })
+}
+
+// 查询时调用接口
+const handleSearch = () => {
+  getTableData()
+}
+
+let tmpSearch = ''
+let tmpMapping = ''
+// 下载
+const getExportTableData = async (type: string, column: any) => {
+  if (column) {
+    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, tmp_mapping: tmpMapping })
+    .then((res: any) => {
+      if (res.code === 200) {
+        allTable.value.data = res.data.Data || []
+        if (allTable.value.data.length > 20000) {
+          type = 'csv'
+        }
+      } else if (res.code === 500) {
+        ElMessage.error(res.data.msg)
+      }
+    })
+    .catch((err: any) => {
+      exportLoading.value = false
+      return
+    })
+  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"
+            @change="getTableData()"
+            placeholder="Please select..."
+          >
+            <el-option v-for="item in sortByOptions" :key="item" :label="item" :value="item" />
+          </el-select>
+          <el-select
+            @change="getTableData()"
+            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>
+          <TableImgEmpty></TableImgEmpty>
+        </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;
+  }
+}
+</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/empty-dark.png


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


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

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

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

@@ -0,0 +1,196 @@
+<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
+        // 初始化的时候如果Report Data Time Range 有值 则调用一次查询
+        changeTimeRange(pageData.value.timeRange)
+      }
+    })
+    .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>

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

@@ -0,0 +1,246 @@
+<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"
+            @change="getTableData()"
+            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>
+          <TableImgEmpty></TableImgEmpty>
+        </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);
+}
+</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>

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

@@ -0,0 +1,523 @@
+<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 8px 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: 8px">
+                  <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>

+ 3 - 3
src/views/SystemSettings/src/SystemSettings.vue

@@ -223,10 +223,10 @@ const deleteAddedRules = (val: any) => {
 // 跳转Create New Rule页面
 const ToCreateRule = () => {
   router.push({
-    path: '/SystemSettings/createnewrule',
+    path: '/system-settings/create-new-rule',
     query: {}
   })
-  sessionStorage.setItem('activeTab', 'Monitoring Settings')
+  sessionStorage.setItem('activeTab', 'System Settings')
 }
 
 const tabledatalength = ref()
@@ -353,7 +353,7 @@ onMounted(() => {
         ref="SubShipmentsTable"
       ></AddRSettingTableules>
     </el-tab-pane>
-    <el-tab-pane label="Monitoring Settings" name="Monitoring Settings">
+    <el-tab-pane label="Monitoring Settings" name="System Settings">
       <div class="monitoring_flex">
         <div class="subscribedTitle">Added Rules</div>
         <el-button

+ 7 - 7
src/views/SystemSettings/src/components/MonitoringTable/src/MonitoringTable.vue

@@ -59,7 +59,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)
@@ -129,10 +129,10 @@ const handleDelete = (row: any) => {
 // 跳转Create New Rule页面
 const ToCreateRule = () => {
   router.push({
-    path: '/SystemSettings/createnewrule',
+    path: '/system-settings/create-new-rule',
     query: {}
   })
-  sessionStorage.setItem('activeTab', 'Monitoring Settings')
+  sessionStorage.setItem('activeTab', 'System Settings')
 }
 
 // 删除表格数据
@@ -152,23 +152,23 @@ const deleteMoniTable = (row: any) => {
 
 // 编辑表格数据
 const handleedit = ({ row }: any) => {
-  sessionStorage.setItem('activeTab', 'Monitoring Settings')
+  sessionStorage.setItem('activeTab', 'System Settings')
   sessionStorage.setItem('editTableid', row.id)
   sessionStorage.setItem('editTablerules_type', row.rules_type)
   sessionStorage.setItem('editTableoption', row.notifications_option)
   router.push({
-    path: '/SystemSettings/createnewrule',
+    path: '/system-settings/create-new-rule',
     query: {}
   })
 }
 
 const handleedittow = (row: any) => {
-  sessionStorage.setItem('activeTab', 'Monitoring Settings')
+  sessionStorage.setItem('activeTab', 'System Settings')
   sessionStorage.setItem('editTableid', row.id)
   sessionStorage.setItem('editTablerules_type', row.rules_type)
   sessionStorage.setItem('editTableoption', row.notifications_option)
   router.push({
-    path: '/SystemSettings/createnewrule',
+    path: '/system-settings/create-new-rule',
     query: {}
   })
 }

+ 1 - 1
src/views/SystemSettings/src/components/SettingTable/src/SettingTable.vue

@@ -115,7 +115,7 @@ const handleDelete = (row: any) => {
 // 点击link字段
 const handleLinkClick = (row: any) => {
   router.push({
-    path: '/shipment/detail',
+    path: '/tracking/detail',
     query: { a: row.__serial_no, _schemas: row._schemas }
   })
   sessionStorage.setItem('activeTab', 'Subscribe Notifications')

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

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

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

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

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

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

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

@@ -0,0 +1,471 @@
+<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 ReportFieldsConfiguration from './components/ReportFieldsConfiguration.vue'
+
+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
+        }
+        oldReportLevel.value = data.reportLevel
+        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
+  mapping?: { system: string; converted: string }[]
+  groupName: string
+  isFieldDataMapping?: 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 accessControlType = ref('All Users')
+const detailRef: Ref<HTMLElement | null> = ref(null)
+
+const specificRoles = ref({
+  partyId: [],
+  groupName: [],
+  systemAccount: []
+})
+
+const changeControlType = () => {
+  specificRoles.value = { 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 handleCancel = () => {
+  router.push('/template-management')
+}
+
+const fieldsConfigurationRef = ref()
+const oldReportLevel = ref('')
+const changeLevel = (val: string) => {
+  fieldsConfigurationRef.value.changeReportLevel(val, oldReportLevel.value)
+}
+const cancelChangeLevel = (val: string) => {
+  infoData.value.reportLevel = val
+}
+const changeOldLevel = (val: string) => {
+  oldReportLevel.value = val
+}
+
+const handlePageSave = () => {
+  let verified = true
+  if (!infoData.value.reportName.trim()) {
+    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 &&
+    !specificRoles.value.groupName?.length &&
+    !specificRoles.value.systemAccount?.length
+  ) {
+    ElMessage.warning(
+      'Please select Party ID or Group Name or KLN ONLINE Account 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: fieldsConfigurationRef.value.getFieldsList()
+  }
+  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"
+                @change="changeLevel"
+                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>
+      <ReportFieldsConfiguration
+        :report-level="infoData.reportLevel"
+        :pageData="fieldsList"
+        @confirm-change-level="changeOldLevel"
+        @cancel-change-level="cancelChangeLevel"
+        ref="fieldsConfigurationRef"
+      ></ReportFieldsConfiguration>
+      <div class="report-access-control template-box">
+        <div class="header">Report Access Control</div>
+        <div class="content-box">
+          <el-radio-group
+            class="radio-group"
+            @change="changeControlType"
+            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>
+  </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);
+}
+
+.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;
+  }
+}
+
+.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);
+          }
+        }
+      }
+    }
+  }
+}
+
+.dashboard {
+  position: relative;
+  background-color: var(--color-mode);
+  .button-group {
+    .el-button {
+      height: 40px;
+      padding: 8px 32px;
+    }
+  }
+}
+</style>

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

@@ -0,0 +1,146 @@
+<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 handleVisibleChange = (visible) => {
+  !visible && (options.value = [])
+}
+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"
+    @visible-change="handleVisibleChange"
+  >
+    <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>

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

@@ -0,0 +1,796 @@
+<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)
+      setTimeout(() => {
+        searchColumn.value = ''
+      }, 600)
+    }
+  }, 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 defaultValue = selectedList?.length && selectedList.length !== 0 ? false : true
+  let paramsData: any = { ...params.value, default: defaultValue }
+  await $api.getReportFieldsConfiguration(paramsData).then((res: any) => {
+    if (res.code === 200) {
+      // allDataCopy就是所有的数据
+      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) => {
+          handleAddSelect(item, false)
+          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, isNeedAdd = true) => {
+  groupColumns.value.forEach((groupItem: any) => {
+    groupItem.children.forEach((child: any, index: number) => {
+      if (child.field === item.field) {
+        groupItem.children.splice(index, 1)
+      }
+    })
+  })
+  isNeedAdd && 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>

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

@@ -0,0 +1,145 @@
+<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 handleVisibleChange = (visible) => {
+  !visible && (options.value = [])
+}
+const remoteMethod = (query: string) => {
+  currentController.value?.abort()
+
+  const newController = new AbortController()
+  currentController.value = newController
+  loading.value = true
+
+  $api
+    .getSpecificRolesPartyID({ term: query }, { signal: newController.signal })
+    .then((res) => {
+      if (!newController.signal.aborted && res.code === 200) {
+        options.value = (res.data || []).map((item) => ({
+          id: item.id,
+          label: item.label
+        }))
+      }
+    })
+    .catch((err) => {
+      options.value = []
+      if (!axios.isCancel(err) && !newController.signal.aborted) {
+        ElMessage.error('Failed to load options')
+      }
+    })
+    .finally(() => {
+      // 仅当这是最新请求时,才关闭 loading
+      if (currentController.value === newController) {
+        loading.value = false
+      }
+    })
+}
+
+// 防抖版本(可选)
+const debouncedRemoteMethod = debounce(remoteMethod, 200)
+
+onUnmounted(() => {
+  currentController.value?.abort()
+})
+
+// 首次聚焦或输入时加载(可选:如果希望空搜也加载)
+// 但通常 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"
+    @visible-change="handleVisibleChange"
+  >
+    <el-option
+      v-for="item in options"
+      :key="item.id + item.label"
+      :label="item.label"
+      :value="item.label"
+    >
+      <div class="select-option">
+        <el-checkbox :model-value="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>

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

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

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

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

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

@@ -0,0 +1,481 @@
+<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, formatTimezoneByUser } 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 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
+  },
+  {
+    title: 'Created By',
+    type: 'normal',
+    field: 'create_by'
+  },
+  {
+    title: 'Modify By',
+    type: 'normal',
+    field: 'modify_by'
+  }
+]
+const tableOriginColumnsField = ref([...tableColumns])
+
+const handleColumns = (columns: any) => {
+  const newColumns = columns.map((item: any) => {
+    let curColumn: any = {
+      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 === 'dateTime') {
+      curColumn = {
+        ...curColumn,
+        formatter: ({ cellValue }: any) => formatTimezoneByUser(cellValue, 'YYYY-MM-DD HH:mm', true)
+      }
+    }
+    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-tooltip effect="dark" content="Edit" placement="top">
+          <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-tooltip>
+
+        <el-tooltip effect="dark" content="Copy" placement="top">
+          <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-tooltip>
+
+        <el-tooltip effect="dark" content="Inactivate" placement="top">
+          <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-tooltip>
+
+        <el-tooltip effect="dark" content="Activate" placement="top">
+          <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-tooltip>
+
+        <el-tooltip effect="dark" content="Delete" placement="top">
+          <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>
+        </el-tooltip>
+      </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>

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

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

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

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

Niektoré súbory nie sú zobrazené, pretože je v týchto rozdielových dátach zmenené mnoho súborov