瀏覽代碼

Merge branch 'test' of United_Software/k_online_ui into master

Jack Zhou 4 月之前
父節點
當前提交
0ce8cfca3d
共有 100 個文件被更改,包括 11485 次插入173 次删除
  1. 2 2
      .env.test
  2. 6 1
      package.json
  3. 7 1
      src/api/index.ts
  4. 238 0
      src/api/module/AIRobot.ts
  5. 88 0
      src/api/module/notificationMessage.ts
  6. 160 0
      src/api/module/system.ts
  7. 2 1
      src/auto-imports.d.ts
  8. 二進制
      src/components/AIRobot/image/icon_ai_robot24_b@2x.png
  9. 二進制
      src/components/AIRobot/image/icon_ai_robot36_b@2x.png
  10. 二進制
      src/components/AIRobot/image/icon_ai_robot48_b@2x.png
  11. 二進制
      src/components/AIRobot/image/icon_cancel_query_b@2x.png
  12. 二進制
      src/components/AIRobot/image/icon_faq_b@2x.png
  13. 1 0
      src/components/AIRobot/index.ts
  14. 349 0
      src/components/AIRobot/src/AIRobot.vue
  15. 1 0
      src/components/AddRules/index.ts
  16. 1115 0
      src/components/AddRules/src/AddRules.vue
  17. 79 0
      src/components/AddRules/src/components/AddedrluesTag.vue
  18. 372 0
      src/components/AddRules/src/components/DelayedType.vue
  19. 503 0
      src/components/AddRules/src/components/ETDShipments.vue
  20. 415 0
      src/components/AddRules/src/components/NotiFrequency.vue
  21. 112 0
      src/components/AddRules/src/components/NotiMethods.vue
  22. 103 0
      src/components/AddRules/src/components/RulesShipments.vue
  23. 209 0
      src/components/AddRules/src/components/ShipmentRange.vue
  24. 二進制
      src/components/AddRules/src/images/icon_collapse.png
  25. 二進制
      src/components/AddRules/src/images/icon_expand.png
  26. 二進制
      src/components/AddRules/src/images/icon_success_big@2x.png
  27. 二進制
      src/components/AddRules/src/images/illustration_email@2x.png
  28. 二進制
      src/components/AddRules/src/images/illustration_system massage@2x.png
  29. 二進制
      src/components/AddRules/src/images/illustration_system massage_darkmode@2x.png
  30. 二進制
      src/components/AddRules/src/images/submit_successful.png
  31. 5 3
      src/components/AutoSelect/src/AutoSelect.vue
  32. 1 0
      src/components/CreateAddRules/index.ts
  33. 1465 0
      src/components/CreateAddRules/src/CreateAddRules.vue
  34. 79 0
      src/components/CreateAddRules/src/components/AddedrluesTag.vue
  35. 376 0
      src/components/CreateAddRules/src/components/DelayedType.vue
  36. 489 0
      src/components/CreateAddRules/src/components/ETDShipments.vue
  37. 416 0
      src/components/CreateAddRules/src/components/NotiFrequency.vue
  38. 100 0
      src/components/CreateAddRules/src/components/NotiMethods.vue
  39. 100 0
      src/components/CreateAddRules/src/components/RulesShipments.vue
  40. 519 0
      src/components/CreateAddRules/src/components/ShipmentRange.vue
  41. 二進制
      src/components/CreateAddRules/src/images/icon_collapse.png
  42. 二進制
      src/components/CreateAddRules/src/images/icon_expand.png
  43. 二進制
      src/components/CreateAddRules/src/images/icon_success_big@2x.png
  44. 二進制
      src/components/CreateAddRules/src/images/illustration_email@2x.png
  45. 二進制
      src/components/CreateAddRules/src/images/illustration_system massage@2x.png
  46. 二進制
      src/components/CreateAddRules/src/images/illustration_system massage_darkmode@2x.png
  47. 二進制
      src/components/CreateAddRules/src/images/submit_successful.png
  48. 54 38
      src/components/DateRange/src/DateRange.vue
  49. 1 1
      src/components/DateRange/src/components/CalendarDate.vue
  50. 9 7
      src/components/DateRange/src/components/QuickCalendarDate.vue
  51. 2 3
      src/components/MoreFilters/src/MoreFilters.vue
  52. 1 0
      src/components/NotificationMessageCard/index.ts
  53. 313 0
      src/components/NotificationMessageCard/src/NotificationMessageCard.vue
  54. 368 0
      src/components/NotificationMessageCard/src/components/EventCard.vue
  55. 164 0
      src/components/NotificationMessageCard/src/components/FeatureUpdateCard.vue
  56. 105 0
      src/components/NotificationMessageCard/src/components/PasswordCard.vue
  57. 二進制
      src/components/NotificationMessageCard/src/images/icon_publish.png
  58. 37 12
      src/components/ScoringGrade/src/ScoringGrade.vue
  59. 2 1
      src/components/SelectTable/src/SelectTable.vue
  60. 4 2
      src/components/SelectTableSelect/src/SelectTableSelect.vue
  61. 2 6
      src/components/ShipmentStatus/src/ShipmentStatus.vue
  62. 46 0
      src/components/TableEmpty/TableEmpty.vue
  63. 二進制
      src/components/TableEmpty/image/default_notification_setting.png
  64. 二進制
      src/components/TableEmpty/image/default_notification_setting@2x.png
  65. 1 0
      src/components/TableEmpty/index.ts
  66. 22 18
      src/components/TransportMode/src/TransportMode.vue
  67. 61 5
      src/components/VBreadcrumb/src/VBreadcrumb.vue
  68. 1 1
      src/components/VLoading/src/VLoading.vue
  69. 0 3
      src/components/VSliderVerification/src/VSliderVerification.vue
  70. 8 4
      src/components/selectAutoSelect/src/selectAutoSelect.vue
  71. 75 3
      src/router/index.ts
  72. 36 2
      src/stores/modules/breadCrumb.ts
  73. 71 0
      src/stores/modules/notificationMessage.ts
  74. 71 8
      src/stores/modules/user.ts
  75. 103 23
      src/styles/elementui.scss
  76. 6 0
      src/styles/index.scss
  77. 135 7
      src/styles/reset.scss
  78. 27 0
      src/styles/theme-g.scss
  79. 127 4
      src/styles/theme.scss
  80. 3 1
      src/styles/vxeTable.scss
  81. 2 6
      src/utils/axios.ts
  82. 27 0
      src/utils/timezone.ts
  83. 105 10
      src/utils/tools.ts
  84. 1 0
      src/views/AIApiLog/index.ts
  85. 195 0
      src/views/AIApiLog/src/AIApiLog.vue
  86. 91 0
      src/views/AIApiLog/src/components/LogDialog.vue
  87. 1 0
      src/views/AIApiLog/src/components/TableView/index.ts
  88. 472 0
      src/views/AIApiLog/src/components/TableView/src/TableView.vue
  89. 196 0
      src/views/AIApiLog/src/components/TableView/src/components/DownloadDialog.vue
  90. 1 0
      src/views/AIRobotChat/index.ts
  91. 848 0
      src/views/AIRobotChat/src/AIRobotChat.vue
  92. 250 0
      src/views/AIRobotChat/src/components/AIQuestions.vue
  93. 83 0
      src/views/AIRobotChat/src/components/AutoResizeTextarea.vue
  94. 46 0
      src/views/AIRobotChat/src/components/LoadingDots.vue
  95. 二進制
      src/views/AIRobotChat/src/image/icon_ai_robot36_b@2x.png
  96. 二進制
      src/views/AIRobotChat/src/image/icon_ai_robot48_b@2x.png
  97. 二進制
      src/views/AIRobotChat/src/image/icon_faq_b@2x.png
  98. 二進制
      src/views/AIRobotChat/src/image/icon_loading.png
  99. 二進制
      src/views/AIRobotChat/src/image/robotBubbleDark.png
  100. 二進制
      src/views/AIRobotChat/src/image/robotBubbleLight.png

+ 2 - 2
.env.test

@@ -1,2 +1,2 @@
-VITE_API_HOST = 'https://ra.kerryapex.com/new/online_backend'
-VITE_BASE_URL = '/new/'
+VITE_API_HOST = '/kln/online_backend'
+VITE_BASE_URL = /kln/

+ 6 - 1
package.json

@@ -11,8 +11,8 @@
     "preview": "vite preview",
     "build-only": "vite build",
     "build:dev": "vite build --mode development",
-    "build:test": "vite build --mode test",
     "build:pro": "vite build --mode product",
+    "build:test": "vite build --mode test",
     "type-check": "vue-tsc --build --force",
     "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
     "format": "prettier --write src/"
@@ -31,8 +31,11 @@
     "echarts": "^5.5.1",
     "element-plus": "^2.8.1",
     "exceljs": "^4.4.0",
+    "github-markdown-css": "^5.8.1",
+    "highlight.js": "^11.11.1",
     "leaflet": "^1.9.4",
     "lodash": "^4.17.21",
+    "markdown-it": "^14.1.0",
     "mitt": "^3.0.1",
     "moment": "^2.30.1",
     "moment-timezone": "^0.5.46",
@@ -40,8 +43,10 @@
     "sass-loader": "^16.0.2",
     "vue": "^3.4.29",
     "vue-draggable-plus": "^0.5.3",
+    "vue-json-pretty": "^2.4.0",
     "vue-router": "^4.3.3",
     "vue3-puzzle-vcode": "^1.1.7",
+    "vue3-virtual-scroller": "^0.2.3",
     "vuedraggable": "^2.24.3",
     "vxe-pc-ui": "^4.1.7",
     "vxe-table": "^4.7.70",

+ 7 - 1
src/api/index.ts

@@ -3,6 +3,9 @@ import * as tracking from './module/tracking'
 import * as common from './module/common'
 import * as login from './module/login'
 import * as other from './module/other'
+import * as notificationMessage from './module/notificationMessage'
+import * as system from './module/system'
+import * as AIRobot from './module/AIRobot'
 /**
  * api 对象接口定义
  */
@@ -19,7 +22,10 @@ const apis = generateApiMap({
   ...tracking,
   ...common,
   ...login,
-  ...other
+  ...other,
+  ...notificationMessage,
+  ...system,
+  ...AIRobot
 })
 export default {
   ...apis // 取出所有可遍历属性赋值在新的对象上

+ 238 - 0
src/api/module/AIRobot.ts

@@ -0,0 +1,238 @@
+import HttpAxios from '@/utils/axios'
+
+const base = import.meta.env.VITE_API_HOST
+const baseUrl = `${base}/main_new_version.php`
+
+/**
+ * Prompt Configuration数据
+ */
+export const getPromptConfiguration = (params: any, config: any) => {
+  return HttpAxios.post(
+    `${baseUrl}`,
+    {
+      action: 'robot',
+      ...params
+    },
+    config
+  )
+}
+/**
+ * 保存Prompt Configuration数据
+ */
+export const SavePromptConfiguration = (params: any, config: any) => {
+  return HttpAxios.post(
+    `${baseUrl}`,
+    {
+      action: 'robot',
+      operate: 'save',
+      ...params
+    },
+    config
+  )
+}
+/**
+ * 编辑返回prompt数据
+ */
+export const EditPrompt = (params: any, config: any) => {
+  return HttpAxios.post(
+    `${baseUrl}`,
+    {
+      action: 'robot',
+      operate: 'preview_propmpt_witout_save',
+      ...params
+    },
+    config
+  )
+}
+/**
+ * AI测试问题
+ */
+export const PromptAITest = (params: any, config: any) => {
+  return HttpAxios.post(
+    `${baseUrl}`,
+    {
+      action: 'robot',
+      operate: 'test_with_ds_claude',
+      ...params
+    },
+    config
+  )
+}
+
+/**
+ * 获取prompt提示器
+ */
+export const getPrompt = (params: any, config: any) => {
+  return HttpAxios.post(
+    `${baseUrl}`,
+    {
+      action: 'robot_chat',
+      operate: 'ai_chat_prompt',
+      ...params
+    },
+    config
+  )
+}
+
+/**
+ * ai聊天
+ */
+export const aiChat = (params: any, config: any) => {
+  return HttpAxios.post(
+    `${baseUrl}`,
+    {
+      action: 'robot_chat',
+      operate: 'ai_chat',
+      ...params
+    },
+    config
+  )
+}
+
+/**
+ * 暂停ai聊天
+ */
+export const pauseAiChat = (params: any, config: any) => {
+  return HttpAxios.post(
+    `${baseUrl}`,
+    {
+      action: 'robot_chat',
+      operate: 'ai_chat_stop',
+      ...params
+    },
+    config
+  )
+}
+
+/**
+ * 评价ai聊天
+ */
+export const feedbackAiChat = (params: any, config: any) => {
+  return HttpAxios.post(
+    `${baseUrl}`,
+    {
+      action: 'robot_chat',
+      operate: 'ai_chat_answer_mark',
+      ...params
+    },
+    config
+  )
+}
+
+/**
+ * AI Robot预设问题显示
+ */
+export const AIRobotInit = (params: any, config: any) => {
+  return HttpAxios.post(
+    `${baseUrl}`,
+    {
+      action: 'robot_chat',
+      operate: 'ai_chat_fixed_init',
+      ...params
+    },
+    config
+  )
+}
+
+/**
+ * 获取chat log 表格列
+ */
+export const getChatLogTableColumn = (params: any, config: any) => {
+  return HttpAxios.post(
+    `${baseUrl}`,
+    {
+      action: 'robot_chat_log',
+      ...params
+    },
+    config
+  )
+}
+
+/**
+ * 根据筛选项获取chat log 表格数据
+ */
+export const getChatLogTableData = (params: any, config: any) => {
+  return HttpAxios.post(
+    `${baseUrl}`,
+    {
+      action: 'robot_chat_log',
+      operate: 'search',
+      ...params
+    },
+    config
+  )
+}
+
+/**
+ * 获取chat log 表格全部数据
+ */
+export const getChatLogAllTableData = (params: any, config: any) => {
+  return HttpAxios.post(
+    `${baseUrl}`,
+    {
+      action: 'robot_chat_log',
+      operate: 'excel',
+      ...params
+    },
+    config
+  )
+}
+
+/**
+ * 获取ai api log 表格列
+ */
+export const getAIApiLogTableColumn = (params: any, config: any) => {
+  return HttpAxios.post(
+    `${baseUrl}`,
+    {
+      action: 'robot_api_log',
+      ...params
+    },
+    config
+  )
+}
+
+/**
+ * 根据筛选项获取ai api log表格数据
+ */
+export const getAIApiLogTableData = (params: any, config: any) => {
+  return HttpAxios.post(
+    `${baseUrl}`,
+    {
+      action: 'robot_api_log',
+      operate: 'search',
+      ...params
+    },
+    config
+  )
+}
+
+/**
+ * 获取ai api log表格全部数据
+ */
+export const getAIApiLogAllTableData = (params: any, config: any) => {
+  return HttpAxios.post(
+    `${baseUrl}`,
+    {
+      action: 'robot_api_log',
+      operate: 'excel',
+      ...params
+    },
+    config
+  )
+}
+
+/**
+ * 获取ai api log弹窗详情
+ */
+export const getAIApiLogDialog = (params: any, config: any) => {
+  return HttpAxios.post(
+    `${baseUrl}`,
+    {
+      action: 'robot_chat_log',
+      operate: 'api_log',
+      ...params
+    },
+    config
+  )
+}

+ 88 - 0
src/api/module/notificationMessage.ts

@@ -0,0 +1,88 @@
+import HttpAxios from '@/utils/axios'
+
+const base = import.meta.env.VITE_API_HOST
+const baseUrl = `${base}/main_new_version.php`
+
+/**
+ * 保存用户个人信息或日期和数字格式化配置
+ * @param save_model profile 代表基本信息的save, no_profile 代表格式信息的save
+ * @param first_name
+ * @param last_name
+ * @param date_fromat
+ * @param numbers_format
+ */
+export const saveUserInfo = (params: any) => {
+  return HttpAxios.post(`${baseUrl}`, {
+    action: 'system_setting',
+    operate: 'personal_profile_save',
+    ...params
+  })
+}
+
+/**
+ * 获取notification消息列表
+ * @param rules_type 特定类型的消息,all查全部
+ * @param current_time 当前时间 轮询查询时使用
+ */
+export const getNotificationList = (params: any) => {
+  return HttpAxios.get(`${baseUrl}`, {
+    action: 'notifications_rules',
+    operate: 'notifications_init',
+    ...params
+  })
+}
+
+/**
+ * 获取notification消息的see all详情
+ */
+export const getNotificationDetails = (params: any) => {
+  return HttpAxios.post(`${baseUrl}`, {
+    action: 'notifications_rules',
+    operate: 'notifications_see_all',
+    ...params
+  })
+}
+
+/**
+ * 获取system message页面数据
+ */
+export const getSystemMessageData = (params: any) => {
+  return HttpAxios.get(`${baseUrl}`, {
+    action: 'notifications_rules',
+    operate: 'notifications_message_init',
+    ...params
+  })
+}
+
+/**
+ * 将notification消息标记为已读
+ * @param id 消息id Array
+ * @param read_type 如果标记全部为已读 = true. 否则其他情况为false
+ */
+export const setMessageRead = (params: any) => {
+  return HttpAxios.post(`${baseUrl}`, {
+    action: 'notifications_rules',
+    operate: 'notifications_read',
+    read_type: false,
+    ...params
+  })
+}
+
+/**
+ * 检测是否有新消息
+ */
+export const hasUnreadMessages = () => {
+  return HttpAxios.post(`${baseUrl}`, {
+    action: 'notifications_rules',
+    operate: 'check_notifications_message'
+  })
+}
+
+/**
+ * 获取feature message详情数据
+ */
+export const getFeatureMsgPdf = () => {
+  return HttpAxios.get(`${baseUrl}`, {
+    action: 'feature_update'
+  })
+}

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

@@ -0,0 +1,160 @@
+import HttpAxios from '@/utils/axios'
+
+const base = import.meta.env.VITE_API_HOST
+const baseUrl = `${base}/main_new_version.php`
+
+
+/**
+ * 获取subscribe数据
+ */
+export const getsubscribe = (params: any, config: any) => {
+  return HttpAxios.get(
+    `${baseUrl}`,
+    {
+      action: 'system_setting',
+      operate: 'subscribe_notification_init',
+      ...params
+    },
+    config
+  )
+}
+/**
+ * 提交subscribe数据
+ */
+export const Savesubscribe = (params: any, config: any) => {
+  return HttpAxios.post(
+    `${baseUrl}`,
+    {
+      action: 'system_setting',
+      operate: 'subscribe_notification_event_update',
+      ...params
+    },
+    config
+  )
+}
+/**
+ * 删除AddedRules表格数据
+ */
+export const DeleteAddedRules = (params: any, config: any) => {
+  return HttpAxios.post(
+    `${baseUrl}`,
+    {
+      action: 'system_setting',
+      operate: 'subscribe_notification_rules_delete',
+      ...params
+    },
+    config
+  )
+}
+/**
+ * 收藏
+ */
+export const SubscribeShipments = (params: any, config: any) => {
+  return HttpAxios.post(
+    `${baseUrl}`,
+    {
+      action: 'system_setting',
+      operate: 'subscribe_shipment',
+      ...params
+    },
+    config
+  )
+}
+/**
+ * 收藏表格分页切换
+ */
+export const SubscribePagination = (params: any, config: any) => {
+  return HttpAxios.post(
+    `${baseUrl}`,
+    {
+      action: 'system_setting',
+      operate: 'subscribe_shipment_search',
+      ...params
+    },
+    config
+  )
+}
+/**
+ * 初始化数据
+ */
+export const MonitoringInit = (params: any, config: any) => {
+  return HttpAxios.get(
+    `${baseUrl}`,
+    {
+      action: 'monitoring_setting',
+      operate: 'monitoring_rules_init',
+      ...params
+    },
+    config
+  )
+}
+/**
+ * Monitoring保存数据
+ */
+export const MonitoringSave = (params: any, config: any) => {
+  return HttpAxios.post(
+    `${baseUrl}`,
+    {
+      action: 'monitoring_setting',
+      operate: 'monitoring_rules_do',
+      ...params
+    },
+    config
+  )
+}
+/**
+ * Monitoring表格数据
+ */
+export const MonitoringTable = (params: any, config: any) => {
+  return HttpAxios.post(
+    `${baseUrl}`,
+    {
+      action: 'monitoring_setting',
+      operate: 'monitoring_rules_search',
+      ...params
+    },
+    config
+  )
+}
+/**
+ * 删除Monitoring表格数据
+ */
+export const deleteMonitoringTable = (params: any, config: any) => {
+  return HttpAxios.post(
+    `${baseUrl}`,
+    {
+      action: 'monitoring_setting',
+      operate: 'monitoring_rules_delete',
+      ...params
+    },
+    config
+  )
+}
+/**
+ * 编辑Monitoring表格数据
+ */
+export const EditMonitoringTable = (params: any, config: any) => {
+  return HttpAxios.post(
+    `${baseUrl}`,
+    {
+      action: 'monitoring_setting',
+      operate: 'monitoring_rules_edit',
+      ...params
+    },
+    config
+  )
+}
+/**
+ * 第一次点击设置页面
+ */
+export const FirstInitSubscribe = (params: any, config: any) => {
+  return HttpAxios.post(
+    `${baseUrl}`,
+    {
+      action: 'system_setting',
+      operate: 'subscribe_notification_default_init',
+      ...params
+    },
+    config
+  )
+}

+ 2 - 1
src/auto-imports.d.ts

@@ -3,6 +3,7 @@
 // @ts-nocheck
 // noinspection JSUnusedGlobalSymbols
 // Generated by unplugin-auto-import
+// biome-ignore lint: disable
 export {}
 declare global {
   const $api: typeof import('@/api/index')['default']
@@ -68,6 +69,6 @@ declare global {
 // for type re-export
 declare global {
   // @ts-ignore
-  export type { Component, ComponentPublicInstance, ComputedRef, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, VNode, WritableComputedRef } from 'vue'
+  export type { Component, ComponentPublicInstance, ComputedRef, DirectiveBinding, ExtractDefaultPropTypes, ExtractPropTypes, ExtractPublicPropTypes, InjectionKey, PropType, Ref, MaybeRef, MaybeRefOrGetter, VNode, WritableComputedRef } from 'vue'
   import('vue')
 }

二進制
src/components/AIRobot/image/icon_ai_robot24_b@2x.png


二進制
src/components/AIRobot/image/icon_ai_robot36_b@2x.png


二進制
src/components/AIRobot/image/icon_ai_robot48_b@2x.png


二進制
src/components/AIRobot/image/icon_cancel_query_b@2x.png


二進制
src/components/AIRobot/image/icon_faq_b@2x.png


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

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

+ 349 - 0
src/components/AIRobot/src/AIRobot.vue

@@ -0,0 +1,349 @@
+<script setup lang="ts">
+import { ref, onMounted } from 'vue'
+import normalPng from '../image/icon_ai_robot24_b@2x.png'
+import emitter from '@/utils/bus'
+
+const clickSrc = ref(normalPng)
+const visible = ref(false)
+const AIRobotHoverVisible = ref(false)
+const clicked = ref(false)
+const isShowDefault = ref(false)
+const isShowAIRobotTop = ref(false)
+const AIIconVisible = ref(false)
+const DeQuestions = ref([])
+const itemGroups = ref([])
+
+const AIRobotInit = () => {
+  $api.AIRobotInit({}).then((res: any) => {
+    DeQuestions.value = res.data.fixed_question
+    prepareGroups()
+  })
+}
+
+// 鼠标hover AIRobot图标
+const AvatarMouseEnter = () => {
+  if (clicked.value) {
+    AIRobotHoverVisible.value = true
+    visible.value = true
+  }
+}
+// 鼠标move AIRobot图标
+const AvatarMouseLeave = () => {
+  if (clicked.value) {
+    visible.value = false
+    AIRobotHoverVisible.value = false
+  }
+}
+const emit = defineEmits(['AvatarClick', 'handelClickAIDefault','handelclickaiinit'])
+// 点击AIRobot图标
+const AvatarClick = () => {
+  if(clicked.value == false) {
+    emit('handelclickaiinit')
+  }
+  clicked.value = true
+  AIRobotHoverVisible.value = false
+  isShowDefault.value = false
+  isShowAIRobotTop.value = false
+  emit('AvatarClick')
+}
+// 隐藏上方弹窗
+const HideAIRobotTop = () => {
+  isShowDefault.value = false
+  isShowAIRobotTop.value = true
+}
+
+// 隐藏上方弹窗
+const HideAIRobotTopTwo = () => {
+  isShowAIRobotTop.value = false
+}
+
+// 随机显示方法
+const prepareGroups = () => {
+  const groups = []
+  let currentGroup = []
+  let currentHeight = 0
+  DeQuestions.value.forEach((item) => {
+    const itemHeight = item.isLong ? 2 : 1
+
+    if (currentHeight + itemHeight > 4) {
+      groups.push(currentGroup)
+      currentGroup = []
+      currentHeight = 0
+    }
+
+    currentGroup.push(item)
+    currentHeight += itemHeight
+  })
+
+  // 添加最后一组
+  if (currentGroup.length > 0) {
+    groups.push(currentGroup)
+  }
+
+  itemGroups.value = groups
+}
+const isShowLogin = () => {
+  AIRobotInit()
+  const loginCount = JSON.parse(localStorage.getItem('userInfo')).loginCount
+  let settimeouttime = 0
+  AIIconVisible.value = true
+  isShowDefault.value = true
+  if (loginCount <= 0) {
+    settimeouttime = 45000
+  } else if (loginCount <= 2) {
+    settimeouttime = 15000
+  } else {
+    settimeouttime = 10000
+  }
+  setTimeout(() => {
+    isShowDefault.value = false
+    isShowAIRobotTop.value = true
+  }, settimeouttime)
+}
+
+// 退出登录后隐藏icon
+const Logout = () => {
+  AIIconVisible.value = false
+  AIRobotHoverVisible.value = false
+  isShowDefault.value = false
+  isShowAIRobotTop.value = false
+  clicked.value = false
+}
+
+// 退出登录后隐藏icon
+const checknoPrompt = () => {
+  AIRobotInit()
+  AIIconVisible.value = true
+}
+
+// 点击问题
+const handelClick = (item: any) => {
+  emit('handelClickAIDefault', item.value)
+  clicked.value = true
+  AIRobotHoverVisible.value = false
+  isShowDefault.value = false
+  isShowAIRobotTop.value = false
+}
+
+onMounted(() => {
+  if(localStorage.getItem('userInfo') != null) {
+    AIIconVisible.value = true
+  }
+  AIRobotInit()
+  emitter.on('login-success', isShowLogin)
+  emitter.on('login-out', Logout)
+  emitter.on('checkPrompt', Logout)
+  emitter.on('checknoPrompt', checknoPrompt)
+})
+
+defineExpose({
+  isShowLogin
+})
+</script>
+<template>
+  <!-- 上方显示的弹窗 -->
+  <div class="AIRobot-top" v-if="isShowDefault">
+    <div class="flex_end" @click="HideAIRobotTop">
+      <div class="icon flex_center icon-AI">
+        <span class="iconfont_icon icon_dark AI_icon">
+          <svg class="iconfont" aria-hidden="true">
+            <use xlink:href="#icon-icon_reject_b"></use>
+          </svg>
+        </span>
+      </div>
+    </div>
+    <div class="flex_title">
+      <div class="AIAvator">
+        <img width="40px" src="../image/icon_ai_robot36_b@2x.png" />
+      </div>
+      <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_title">
+          <div class="dialogue_title_left">
+            <img src="../image/icon_faq_b@2x.png" width="24px" />
+            Frequently Asked Questions
+          </div>
+        </div>
+        <el-carousel class="carousel" :autoplay="false" height="190px" style="width: 452px">
+          <el-carousel-item v-for="(group, index) in itemGroups" :key="index">
+            <div class="dialogue_container">
+              <div
+                class="dialogue_content_item"
+                @click="handelClick(item)"
+                v-for="item in group"
+                :key="item.label"
+                :class="{ long_item: item.isLong }"
+              >
+                {{ item.label }}
+              </div>
+            </div>
+          </el-carousel-item>
+        </el-carousel>
+      </div>
+    </div>
+  </div>
+  <div class="AIRobot-top" v-if="isShowAIRobotTop">
+    <div class="flex_end" @click="HideAIRobotTopTwo">
+      <div class="icon flex_center icon-AI">
+        <span class="iconfont_icon icon_dark AI_icon">
+          <svg class="iconfont" aria-hidden="true">
+            <use xlink:href="#icon-icon_reject_b"></use>
+          </svg>
+        </span>
+      </div>
+    </div>
+    <div class="dialogue_title" style="margin-bottom: 0">
+      Hi! I'm your Freight Assistant, always on call
+    </div>
+  </div>
+  <!-- 悬浮icon -->
+  <div class="AIRobot flex_center" v-if="AIIconVisible">
+    <el-popover :visible="visible" placement="top-end" width="auto">
+      <template #reference>
+        <el-avatar
+          @click="AvatarClick"
+          @mouseenter="AvatarMouseEnter"
+          @mouseleave="AvatarMouseLeave"
+          :size="46"
+          shape="square"
+          class="avatar_bg"
+          :src="clickSrc"
+        />
+      </template>
+      <!-- hover时显示的对话框 -->
+      <div v-if="AIRobotHoverVisible" class="AIRobot_dialog">Continue the conversation</div>
+    </el-popover>
+  </div>
+</template>
+
+<style lang="scss">
+.AI_icon {
+  margin-right: 0;
+}
+.flex_center {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+}
+.flex_end {
+  display: flex;
+  justify-content: end;
+}
+.flex_title {
+  display: flex;
+  align-items: center;
+  width: 500px;
+}
+.AIRobot-top {
+  position: absolute;
+  z-index: 1999;
+  right: 35px;
+  bottom: 188px;
+}
+.dialogue_content_title {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  padding-left: 8px;
+}
+.dialogue_title_left {
+  background: var(--color-dialogue_title);
+  -webkit-background-clip: text;
+  background-clip: text;
+  color: transparent;
+  font-size: 14px;
+  font-weight: 700;
+}
+.dialogue_title_right {
+  color: var(--color-theme);
+  cursor: pointer;
+  user-select: none;
+}
+.icon_theme {
+  fill: var(--color-theme);
+  cursor: pointer;
+}
+.icon-AI {
+  width: 24px;
+  height: 24px;
+  background-color: var(--management-bg-color);
+  box-shadow: -2px 2px 12px rgba(0, 0, 0, 15%);
+  border-radius: 6px;
+  cursor: pointer;
+}
+.AIRobot {
+  width: 50px;
+  height: 50px;
+  border-radius: 12px;
+  background: var(--color-dialogue-icon-bg);
+  position: absolute;
+  box-shadow: -2px 2px 12px rgba(0, 0, 0, 15%);
+  z-index: 1999;
+  right: 10px;
+  bottom: 130px;
+  img {
+    width: 36px;
+    height: 36px;
+  }
+}
+.AIRobot_dialog {
+  background: var(--color-dialogue-text-bg);
+  box-shadow: -10px 10px 24px 0px rgba(58, 0, 78, 0.15);
+  padding: 5px 8px;
+  border-radius: 3px;
+}
+.dialogue_title {
+  background: var(--color-dialogue-text-bg);
+  box-shadow: -10px 10px 24px rgba(58, 0, 78, 0.15);
+  padding: 8px;
+  border-radius: 12px;
+  margin: 10px 0;
+  width: 294px;
+}
+.dialogue_content {
+  background: var(--color-dialogue-text-bg);
+  padding: 8px 0 0 0;
+  border-radius: 12px;
+}
+.AIAvator {
+  width: 40px;
+  height: 40px;
+  background: var(--color-dialogue-icon-bg);
+  border-radius: 12px;
+  margin-right: 8px;
+}
+.dialogue_container {
+  width: 420px;
+  height: 156px;
+  border-radius: 12px;
+  border: 1px solid var(--color-dialogue_container-border);
+  padding: 8px;
+  background: var(--color-dialogue_container-bg);
+}
+.dialogue_content_item {
+  width: 404px;
+  height: 32px;
+  padding: 5.5px 8px;
+  border-radius: 6px;
+  background-color: var(--color-dialogue-bg);
+  margin-bottom: 4px;
+  display: flex;
+  align-items: center;
+  word-break: break-word;
+  overflow: hidden;
+  transition: all 0.3s ease;
+  cursor: pointer;
+}
+.dialogue_content_item:hover {
+  background-color: var(--color-arrow-hoverL);
+  color: var(--color-theme);
+}
+.itemLable {
+  width: 100%;
+}
+.long_item {
+  height: 64px;
+}
+</style>

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

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

+ 1115 - 0
src/components/AddRules/src/AddRules.vue

@@ -0,0 +1,1115 @@
+<script lang="ts" setup>
+import { ref, watch } from 'vue'
+import RulesShipments from './components/RulesShipments.vue'
+import AddedrluesTag from './components/AddedrluesTag.vue'
+import DelayedType from './components/DelayedType.vue'
+import ETDShipments from './components/ETDShipments.vue'
+import NotiFrequency from './components/NotiFrequency.vue'
+import NotiMethods from './components/NotiMethods.vue'
+import submitsucessful from './images/icon_success_big@2x.png'
+import moment from 'moment-timezone'
+interface CheckboxItem {
+  value: string
+  label: string
+}
+
+interface Props {
+  SystemList: Object
+  TitleType: String
+}
+const MilestoneOceanListInit = ref<CheckboxItem[]>([])
+const MilestoneOceanListChecked = ref([])
+const MilestoneAirListInit = ref<CheckboxItem[]>([])
+const MilestoneAirListChecked = ref([])
+const ContainerOceanListInit = ref<CheckboxItem[]>([])
+const ContainerOceanListChecked = ref([])
+const props = defineProps<Props>()
+let savesubscribeobj: any = {}
+const SystemList = ref(props.SystemList)
+const RulesActive = ref(['SelectMilestone', 'NotificationFrequency', 'NotificationMethod'])
+const OceanCheckList = ref([])
+const OceanCheckCode = ref()
+const AirCheckList = ref([])
+const AirCheckListCode = ref()
+const ContainerOceanList = ref([])
+const ContainerOceanCode = ref()
+const IsFirstActive = ref(true)
+const IsTwoActive = ref(true)
+const IsThreeActive = ref(true)
+const CancelRulesVisible = ref(false)
+const UnableSaveVisible = ref(false)
+const SaveedVisible = ref(false)
+const DelayedDeparturedList = ref([])
+const DelayedAirdList = ref([])
+const ETDOceanList = ref([])
+const ETDAirList = ref([])
+const FrequencyDataMil = ref()
+const FrequencyDataCon = ref()
+const FrequencyDataDep = ref()
+const FrequencyDataETD = ref()
+const MethodsDataMil = ref()
+const MethodsDataCon = ref()
+const MethodsDataDep = ref()
+const MethodsDataETD = ref()
+const DelayedDataInit = ref()
+const DelayedDataInitAir = ref()
+const OceanETDInit = ref()
+const AirETDInit = ref()
+
+watch(
+  () => props.SystemList,
+  (current) => {
+    SystemList.value = current
+    FrequencyDataMil.value = current['Milestone_Update']
+    MethodsDataMil.value = current['Milestone_Update']
+    FrequencyDataCon.value = current['Container_Status_Update']
+    MethodsDataCon.value = current['Container_Status_Update']
+    FrequencyDataDep.value = current['Departure/Arrival_Delay']
+    MethodsDataDep.value = current['Departure/Arrival_Delay']
+    FrequencyDataETD.value = current['ETD/ETA_Change']
+    MethodsDataETD.value = current['ETD/ETA_Change']
+    Initdata(current)
+  }
+)
+
+// 初始赋值
+const Initdata = (val: any) => {
+  MilestoneOceanListInit.value = val.Milestone_Update.OceanCheckBoxList
+  OceanCheckList.value = []
+  OceanCheckCode.value = []
+  MilestoneOceanListChecked.value = val.Milestone_Update.OceanCheckedList
+  OceanCheckCode.value = val.Milestone_Update.OceanCheckedList
+  // 遍历选中的value值,找到对应的label值
+  MilestoneOceanListChecked.value.forEach((value) => {
+    const option = MilestoneOceanListInit.value.find((item) => item.value === value)
+    if (option) {
+      OceanCheckList.value.push(option.label)
+    }
+  })
+  AirCheckListCode.value = []
+  MilestoneAirListInit.value = val.Milestone_Update.AirCheckBoxList
+  MilestoneAirListChecked.value = val.Milestone_Update.AirCheckedList
+  AirCheckListCode.value = val.Milestone_Update.AirCheckedList
+  AirCheckList.value = []
+  // 遍历选中的value值,找到对应的label值
+  MilestoneAirListChecked.value.forEach((value) => {
+    const option = MilestoneAirListInit.value.find((item) => item.value === value)
+    if (option) {
+      AirCheckList.value.push(option.label)
+    }
+  })
+  ContainerOceanCode.value = []
+  ContainerOceanListInit.value = val.Container_Status_Update.CtnrCheckBoxList
+  ContainerOceanListChecked.value = val.Container_Status_Update.CtnrCheckedList
+  ContainerOceanCode.value = val.Container_Status_Update.CtnrCheckedList
+  ContainerOceanList.value = []
+  // 遍历选中的value值,找到对应的label值
+  ContainerOceanListChecked.value.forEach((value) => {
+    const option = ContainerOceanListInit.value.find((item) => item.value === value)
+    if (option) {
+      ContainerOceanList.value.push(option.label)
+    }
+  })
+  let OceanObj: any = {}
+  OceanObj.atd_etd = val['Departure/Arrival_Delay'].ocean_atd_sub_etd
+  OceanObj.atd_etd_unit = val['Departure/Arrival_Delay'].ocean_atd_sub_etd_unit
+  OceanObj.ata_eta = val['Departure/Arrival_Delay'].ocean_ata_sub_eta
+  OceanObj.ata_eta_unit = val['Departure/Arrival_Delay'].ocean_ata_sub_eta_unit
+  DelayedDataInit.value = OceanObj
+  let AirObj: any = {}
+  AirObj.atd_etd = val['Departure/Arrival_Delay'].air_atd_sub_etd
+  AirObj.atd_etd_unit = val['Departure/Arrival_Delay'].air_atd_sub_etd_unit
+  AirObj.ata_eta = val['Departure/Arrival_Delay'].air_ata_sub_eta
+  AirObj.ata_eta_unit = val['Departure/Arrival_Delay'].air_ata_sub_eta_unit
+  DelayedDataInitAir.value = AirObj
+  let OceanChange: any = {}
+  OceanChange.ETDradio = val['ETD/ETA_Change'].ocean_etd_change
+  OceanChange.etd_old_sub_new = val['ETD/ETA_Change'].ocean_etd_old_sub_new
+  OceanChange.etd_old_sub_new_unit = val['ETD/ETA_Change'].ocean_etd_old_sub_new_unit
+  OceanChange.ETAradio = val['ETD/ETA_Change'].ocean_eta_change
+  OceanChange.eta_old_sub_new = val['ETD/ETA_Change'].ocean_eta_old_sub_new
+  OceanChange.eta_old_sub_new_unit = val['ETD/ETA_Change'].ocean_eta_old_sub_new_unit
+  OceanETDInit.value = OceanChange
+  let AirChange: any = {}
+  AirChange.ETDradio = val['ETD/ETA_Change'].air_etd_change
+  AirChange.etd_old_sub_new = val['ETD/ETA_Change'].air_etd_old_sub_new
+  AirChange.etd_old_sub_new_unit = val['ETD/ETA_Change'].air_etd_old_sub_new_unit
+  AirChange.ETAradio = val['ETD/ETA_Change'].air_eta_change
+  AirChange.eta_old_sub_new = val['ETD/ETA_Change'].air_eta_old_sub_new
+  AirChange.eta_old_sub_new_unit = val['ETD/ETA_Change'].air_eta_old_sub_new_unit
+  AirETDInit.value = AirChange
+}
+
+// 给tag list赋值
+const ChangeCheckOceanRules = (val: any, value: any) => {
+  OceanCheckList.value = val
+  OceanCheckCode.value = value
+}
+const ChangeContainerRules = (val: any, value: any) => {
+  ContainerOceanList.value = val
+  ContainerOceanCode.value = value
+}
+// delayed赋值
+const ChangeDeayedRules = (val: any) => {
+  DelayedDeparturedList.value = []
+  if (val.Departure != '') {
+    DelayedDeparturedList.value.push(val.Departure)
+    savesubscribeobj.ocean_atd_sub_etd = val.Departure.split(' ')[4]
+    savesubscribeobj.ocean_atd_sub_etd_unit = val.Departure.split(' ')[5]
+  } else {
+    delete savesubscribeobj.ocean_atd_sub_etd
+    delete savesubscribeobj.ocean_atd_sub_etd_unit
+  }
+  if (val.Arrival != '') {
+    DelayedDeparturedList.value.push(val.Arrival)
+    savesubscribeobj.ocean_ata_sub_eta = val.Arrival.split(' ')[4]
+    savesubscribeobj.ocean_ata_sub_eta_unit = val.Arrival.split(' ')[5]
+  } else {
+    delete savesubscribeobj.ocean_ata_sub_eta
+    delete savesubscribeobj.ocean_ata_sub_eta_unit
+  }
+}
+const ChangeAirRules = (val: any) => {
+  DelayedAirdList.value = []
+  if (val.Departure != '') {
+    DelayedAirdList.value.push(val.Departure)
+    savesubscribeobj.air_atd_sub_etd = val.Departure.split(' ')[4]
+    savesubscribeobj.air_atd_sub_etd_unit = val.Departure.split(' ')[5]
+  } else {
+    delete savesubscribeobj.air_atd_sub_etd
+    delete savesubscribeobj.air_atd_sub_etd_unit
+  }
+  if (val.Arrival != '') {
+    DelayedAirdList.value.push(val.Arrival)
+    savesubscribeobj.air_ata_sub_eta = val.Arrival.split(' ')[4]
+    savesubscribeobj.air_ata_sub_eta_unit = val.Arrival.split(' ')[5]
+  } else {
+    delete savesubscribeobj.air_ata_sub_eta
+    delete savesubscribeobj.air_ata_sub_eta_unit
+  }
+}
+const ChangeCheckAirRules = (val: any, value: any) => {
+  AirCheckList.value = val
+  AirCheckListCode.value = value
+}
+
+// 更改Frequency时间
+const MilFrequencyList = ref()
+const ChangeMilFrequency = (val: any, type: any) => {
+  MilFrequencyList.value = val
+  savesubscribeobj = { ...savesubscribeobj, ...type }
+}
+const ConFrequencyList = ref()
+const ChangeConFrequency = (val: any, type: any) => {
+  ConFrequencyList.value = val
+  savesubscribeobj = { ...savesubscribeobj, ...type }
+}
+const DepFrequencyList = ref()
+const ChangeDepFrequency = (val: any, type: any) => {
+  DepFrequencyList.value = val
+  savesubscribeobj = { ...savesubscribeobj, ...type }
+}
+const ETDFrequencyList = ref()
+const ChangeETDFrequency = (val: any, type: any) => {
+  ETDFrequencyList.value = val
+  savesubscribeobj = { ...savesubscribeobj, ...type }
+}
+
+// 删除 Frequency tag
+const NotiFrequencyDeleteMil = ref()
+const NotiFrequencyDeleteCon = ref()
+const NotiFrequencyDeleteDep = ref()
+const NotiFrequencyDeleteETD = ref()
+const handleCloseRadio = (val: any) => {
+  if (val == 'Mil') {
+    NotiFrequencyDeleteMil.value.handleCloseRadioFrequency()
+  } else if (val == 'Con') {
+    NotiFrequencyDeleteCon.value.handleCloseRadioFrequency()
+  } else if (val == 'Dep') {
+    NotiFrequencyDeleteDep.value.handleCloseRadioFrequency()
+  } else {
+    NotiFrequencyDeleteETD.value.handleCloseRadioFrequency()
+  }
+}
+
+// 删除 Frequency tag
+const NotimethodsMil = ref()
+const NotimethodsCon = ref()
+const NotimethodsDep = ref()
+const NotimethodsETD = ref()
+const handleCloseMethods = (val: any) => {
+  if (val == 'Mil') {
+    NotimethodsMil.value.handleCloseMethods()
+  } else if (val == 'Con') {
+    NotimethodsCon.value.handleCloseMethods()
+  } else if (val == 'Dep') {
+    NotimethodsDep.value.handleCloseMethods()
+  } else {
+    NotimethodsETD.value.handleCloseMethods()
+  }
+}
+
+// methods 切换
+const MilMethodsList = ref()
+const ChangeMethodsAddMil = (val: any, type: any) => {
+  MilMethodsList.value = val
+  savesubscribeobj = { ...savesubscribeobj, ...type }
+}
+const ConMethodsList = ref()
+const ChangeMethodsAddCon = (val: any, type: any) => {
+  ConMethodsList.value = val
+  savesubscribeobj = { ...savesubscribeobj, ...type }
+}
+const DepMethodsList = ref()
+const ChangeMethodsAddDep = (val: any, type: any) => {
+  DepMethodsList.value = val
+  savesubscribeobj = { ...savesubscribeobj, ...type }
+}
+const ETDMethodsList = ref()
+const ChangeMethodsAddETD = (val: any, type: any) => {
+  ETDMethodsList.value = val
+  savesubscribeobj = { ...savesubscribeobj, ...type }
+}
+const OceanDelayed = ref()
+const AirDelayed = ref()
+
+const handleCloseDelayed = (val: any) => {
+  OceanDelayed.value.closeDelayed(val)
+}
+const handleCloseAirDelayed = (val: any) => {
+  AirDelayed.value.closeDelayed(val)
+}
+// ETD Change
+const ChangeETDOceanRules = (val: any) => {
+  ETDOceanList.value = []
+  if (val.ETD != '') {
+    if (val.ETD.indexOf('≥') != -1) {
+      savesubscribeobj.ocean_etd_change = false
+      savesubscribeobj.ocean_etd_old_sub_new = val.ETD.split(' ')[7]
+      savesubscribeobj.ocean_etd_old_sub_new_unit = val.ETD.split(' ')[8]
+    } else {
+      savesubscribeobj.ocean_etd_change = true
+    }
+    ETDOceanList.value.push(val.ETD)
+  } else {
+    delete savesubscribeobj.ocean_etd_change
+    delete savesubscribeobj.ocean_etd_old_sub_new
+    delete savesubscribeobj.ocean_etd_old_sub_new_unit
+  }
+  if (val.ETA != '') {
+    if (val.ETA.indexOf('≥') != -1) {
+      savesubscribeobj.ocean_eta_change = false
+      savesubscribeobj.ocean_eta_old_sub_new = val.ETA.split(' ')[7]
+      savesubscribeobj.ocean_eta_old_sub_new_unit = val.ETA.split(' ')[8]
+    } else {
+      savesubscribeobj.ocean_eta_change = true
+    }
+    ETDOceanList.value.push(val.ETA)
+  } else {
+    delete savesubscribeobj.ocean_eta_change
+    delete savesubscribeobj.ocean_eta_old_sub_new
+    delete savesubscribeobj.ocean_eta_old_sub_new_unit
+  }
+}
+// 删除ETD tag
+const OceanETD = ref()
+const closeOceanETD = (val: any) => {
+  OceanETD.value.closeETD(val)
+}
+// ETA Change
+const ChangeETDAirRules = (val: any) => {
+  ETDAirList.value = []
+  if (val.ETD != '') {
+    if (val.ETD.indexOf('≥') != -1) {
+      savesubscribeobj.air_etd_change = false
+      savesubscribeobj.air_etd_old_sub_new = val.ETD.split(' ')[7]
+      savesubscribeobj.air_etd_old_sub_new_unit = val.ETD.split(' ')[8]
+    } else {
+      savesubscribeobj.air_etd_change = true
+    }
+    ETDAirList.value.push(val.ETD)
+  } else {
+    delete savesubscribeobj.air_etd_change
+    delete savesubscribeobj.air_etd_old_sub_new
+    delete savesubscribeobj.air_etd_old_sub_new_unit
+  }
+  if (val.ETA != '') {
+    if (val.ETA.indexOf('≥') != -1) {
+      savesubscribeobj.air_eta_change = false
+      savesubscribeobj.air_eta_old_sub_new = val.ETA.split(' ')[7]
+      savesubscribeobj.air_eta_old_sub_new_unit = val.ETA.split(' ')[8]
+    } else {
+      savesubscribeobj.air_eta_change = true
+    }
+    ETDAirList.value.push(val.ETA)
+  } else {
+    delete savesubscribeobj.air_eta_change
+    delete savesubscribeobj.air_eta_old_sub_new
+    delete savesubscribeobj.air_eta_old_sub_new_unit
+  }
+}
+
+// 删除ETA tag
+const AirETD = ref()
+const closeAirETD = (val: any) => {
+  AirETD.value.closeETD(val)
+}
+
+const emits = defineEmits(['UnsavedCollapse', 'SavedAddedRules'])
+// 不保存修改的折叠面板
+const UnsavedCollapse = () => {
+  CancelRulesVisible.value = false
+  emits('UnsavedCollapse')
+}
+
+// 保存subscribe配置
+const missingmessage = ref('')
+let defaultTimeZone = 'UTC' + moment().tz(moment.tz.guess()).format('Z')
+// 保存成功调用接口
+const SaveSuceessful = () => {
+  $api
+    .Savesubscribe({
+      ...savesubscribeobj,
+      default_time_zone: defaultTimeZone
+    })
+    .then((res: any) => {
+      if (res.code === 200) {
+        SaveedVisible.value = true
+        setTimeout(() => {
+          SaveedVisible.value = false
+          emits('SavedAddedRules', res.data.addedRules, savesubscribeobj.rules_type)
+        }, 3000)
+      }
+    })
+}
+const Savesubscribe = () => {
+  let str = ''
+  missingmessage.value = ''
+  if (props.TitleType == 'Milestone') {
+    savesubscribeobj.rules_type = 'Milestone_Update'
+    if (
+      OceanCheckList.value.length == 0 &&
+      AirCheckList.value.length == 0 ||
+      MilFrequencyList.value.length == 0 ||
+      MilMethodsList.value == undefined ||
+      MilMethodsList.value.length == 0
+    ) {
+      if (OceanCheckList.value.length == 0 && AirCheckList.value.length == 0) {
+        missingmessage.value += 'Select Milestone, '
+      }
+      if (MilFrequencyList.value.length == 0) {
+        missingmessage.value += 'Notification Frequency, '
+      }
+      if (MilMethodsList.value.length == 0 || MilMethodsList.value == undefined) {
+        missingmessage.value += 'Notification Method, '
+      }
+      missingmessage.value = missingmessage.value.substring(0, missingmessage.value.length - 2)
+      UnableSaveVisible.value = true
+    } else {
+      savesubscribeobj.ocean_milestone = OceanCheckCode.value
+      savesubscribeobj.air_milestone = AirCheckListCode.value
+      if(OceanCheckList.value.length == 0) {
+        str =
+        'Air Milestones: ' +
+        AirCheckList.value.join(',')
+      } else if(AirCheckList.value.length == 0) {
+        str =
+        'Ocean Milestones: ' +
+        OceanCheckList.value.join(',')
+      } else {
+        str =
+        'Ocean Milestones: ' +
+        OceanCheckList.value.join(',') +
+        ';\nAir Milestones: ' +
+        AirCheckList.value.join(',') +
+        ';'
+      }
+      savesubscribeobj.event_details = str
+      SaveSuceessful()
+    }
+  } else if (props.TitleType == 'Container') {
+    savesubscribeobj.rules_type = 'Container_Status_Update'
+    if (
+      ContainerOceanList.value.length == 0 ||
+      ConFrequencyList.value.length == 0 ||
+      ConMethodsList.value == undefined ||
+      ConMethodsList.value.length == 0
+    ) {
+      if (ContainerOceanList.value.length == 0) {
+        missingmessage.value += 'Container Status, '
+      }
+      if (ConFrequencyList.value.length == 0) {
+        missingmessage.value += 'Notification Frequency, '
+      }
+      if (ConMethodsList.value.length == 0 || ConMethodsList.value == undefined) {
+        missingmessage.value += 'Notification Method, '
+      }
+      missingmessage.value = missingmessage.value.substring(0, missingmessage.value.length - 2)
+      UnableSaveVisible.value = true
+    } else {
+      savesubscribeobj.ocean_ctnr_status = ContainerOceanCode.value
+      str = 'Ocean Container: ' + ContainerOceanList.value.join(',')
+      savesubscribeobj.event_details = str
+      SaveSuceessful()
+    }
+  } else if (props.TitleType == 'Departure') {
+    savesubscribeobj.rules_type = 'Departure/Arrival_Delay'
+    if (
+      DelayedDeparturedList.value.length == 0 &&
+      DelayedAirdList.value.length == 0 ||
+      DepFrequencyList.value.length == 0 ||
+      DepMethodsList.value == undefined ||
+      DepMethodsList.value.length == 0
+    ) {
+      if (DelayedDeparturedList.value.length == 0 && DelayedAirdList.value.length == 0) {
+        missingmessage.value += 'Select Delayed Type, '
+      }
+      if (DepFrequencyList.value.length == 0) {
+        missingmessage.value += 'Notification Frequency, '
+      }
+      if (DepMethodsList.value.length == 0 || DepMethodsList.value == undefined) {
+        missingmessage.value += 'Notification Method, '
+      }
+      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(',')
+      } else {
+        str =
+        'Ocean: ' +
+        DelayedDeparturedList.value.join(',') +
+        ';\nAir: ' +
+        DelayedAirdList.value.join(',') +
+        ';'
+      }
+      savesubscribeobj.event_details = str
+      SaveSuceessful()
+    }
+  } else {
+    savesubscribeobj.rules_type = 'ETD/ETA_Change'
+    if (
+      ETDOceanList.value.length == 0 &&
+      ETDAirList.value.length == 0 ||
+      ETDFrequencyList.value.length == 0 ||
+      ETDMethodsList.value == undefined ||
+      ETDMethodsList.value.length == 0
+    ) {
+      if (ETDOceanList.value.length == 0 && ETDAirList.value.length == 0) {
+        missingmessage.value += 'Select Time Type, '
+      }
+      if (ETDFrequencyList.value.length == 0) {
+        missingmessage.value += 'Notification Frequency, '
+      }
+      if (ETDMethodsList.value.length == 0 || ETDMethodsList.value == undefined) {
+        missingmessage.value += 'Notification Method, '
+      }
+      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(',')
+      } else {
+        str =
+        '[Ocean]' +
+        ETDOceanList.value.join(',') +
+        ';\n[Air]' +
+        ETDAirList.value.join(',') +
+        ';'
+      }
+      savesubscribeobj.event_details = str
+      SaveSuceessful()
+    }
+  }
+}
+
+// 删除表格数据后清空所有数据
+const clearData = (val: any) => {
+  if (val == 'Milestone_Update') {
+    OceanCheckList.value = []
+    AirCheckList.value = []
+    MilFrequencyList.value = []
+    MilMethodsList.value = []
+    MilestoneOceanListChecked.value = []
+    MilestoneAirListChecked.value = []
+    handleCloseRadio('Mil')
+    handleCloseMethods('Mil')
+  } else if (val == 'Container_Status_Update') {
+    ContainerOceanList.value = []
+    ConFrequencyList.value = []
+    ConMethodsList.value = []
+    ContainerOceanListChecked.value = []
+    handleCloseRadio('Con')
+    handleCloseMethods('Con')
+  } else if (val == 'Departure/Arrival_Delay') {
+    DelayedDeparturedList.value = []
+    DelayedAirdList.value = []
+    DepFrequencyList.value = []
+    DepMethodsList.value = []
+    OceanDelayed.value.ClearData()
+    AirDelayed.value.ClearData()
+    handleCloseRadio('Dep')
+    handleCloseMethods('Dep')
+  } else {
+    ETDOceanList.value = []
+    ETDAirList.value = []
+    ETDFrequencyList.value = []
+    ETDMethodsList.value = []
+    handleCloseRadio('ETD')
+    handleCloseMethods('ETD')
+    OceanETD.value.ClearData()
+    AirETD.value.ClearData()
+  }
+}
+
+// 删除Oceam shipments
+const MilOceanref = ref()
+const MilAirref = ref()
+const ContainerOcean = ref()
+const handleCloseMilestoneOcean = (val:any) => {
+  MilOceanref.value.hadleclose(val)
+}
+const handleCloseMilestoneAir = (val:any) => {
+  MilAirref.value.hadleclose(val)
+}
+
+const handleCloseContainer = (val:any) => {
+  ContainerOcean.value.hadleclose(val)
+}
+
+defineExpose({
+  clearData
+})
+</script>
+<template>
+  <div class="Rules_flex">
+    <div class="Rules_left">
+      <div class="Rules_collapse">
+        <el-collapse
+          v-model="RulesActive"
+          @change="IsFirstActive = !IsFirstActive"
+          v-if="props.TitleType == 'Milestone'"
+        >
+          <el-collapse-item name="SelectMilestone">
+            <template #title>
+              <div class="Rules_Title">
+                <span class="iconfont_icon icon_dark">
+                  <svg class="iconfont" aria-hidden="true">
+                    <use
+                      :xlink:href="IsFirstActive ? '#icon-icon_dropdown_b' : '#icon-icon_up_b'"
+                    ></use>
+                  </svg>
+                </span>
+                <span class="stars_red">*</span>Select Milestone
+              </div>
+            </template>
+            <div>
+              <RulesShipments
+                Title="Ocean Shipments"
+                ref="MilOceanref"
+                @ChangeCheckRules="ChangeCheckOceanRules"
+                :CheckboxList="MilestoneOceanListInit"
+                :CheckedList="MilestoneOceanListChecked"
+              ></RulesShipments>
+            </div>
+            <div>
+              <RulesShipments
+                Title="Air Shipments"
+                ref="MilAirref"
+                @ChangeCheckRules="ChangeCheckAirRules"
+                :CheckboxList="MilestoneAirListInit"
+                :CheckedList="MilestoneAirListChecked"
+              ></RulesShipments>
+            </div>
+          </el-collapse-item>
+        </el-collapse>
+        <el-collapse
+          v-model="RulesActive"
+          @change="IsFirstActive = !IsFirstActive"
+          v-if="props.TitleType == 'Container'"
+        >
+          <el-collapse-item name="SelectMilestone">
+            <template #title>
+              <div class="Rules_Title">
+                <span class="iconfont_icon icon_dark">
+                  <svg class="iconfont" aria-hidden="true">
+                    <use
+                      :xlink:href="IsFirstActive ? '#icon-icon_dropdown_b' : '#icon-icon_up_b'"
+                    ></use>
+                  </svg>
+                </span>
+                <span class="stars_red">*</span>Select Container Status
+              </div>
+            </template>
+            <div>
+              <RulesShipments
+                Title="Ocean Shipments"
+                ref="ContainerOcean"
+                @ChangeCheckRules="ChangeContainerRules"
+                :CheckboxList="ContainerOceanListInit"
+                :CheckedList="ContainerOceanListChecked"
+              ></RulesShipments>
+            </div>
+          </el-collapse-item>
+        </el-collapse>
+        <el-collapse
+          v-model="RulesActive"
+          @change="IsFirstActive = !IsFirstActive"
+          v-if="props.TitleType == 'Departure'"
+        >
+          <el-collapse-item name="SelectMilestone">
+            <template #title>
+              <div class="Rules_Title">
+                <span class="iconfont_icon icon_dark">
+                  <svg class="iconfont" aria-hidden="true">
+                    <use
+                      :xlink:href="IsFirstActive ? '#icon-icon_dropdown_b' : '#icon-icon_up_b'"
+                    ></use>
+                  </svg>
+                </span>
+                <span class="stars_red">*</span>Select Delayed Type
+              </div>
+            </template>
+            <div>
+              <DelayedType
+                Title="Ocean Shipments"
+                ref="OceanDelayed"
+                :DelayedData="DelayedDataInit"
+                @ChangeCheckRules="ChangeDeayedRules"
+              ></DelayedType>
+            </div>
+            <div>
+              <DelayedType
+                Title="Air Shipments"
+                ref="AirDelayed"
+                :DelayedData="DelayedDataInitAir"
+                @ChangeCheckRules="ChangeAirRules"
+              ></DelayedType>
+            </div>
+          </el-collapse-item>
+        </el-collapse>
+        <el-collapse
+          v-model="RulesActive"
+          @change="IsFirstActive = !IsFirstActive"
+          v-if="props.TitleType == 'ETDChange'"
+        >
+          <el-collapse-item name="SelectMilestone">
+            <template #title>
+              <div class="Rules_Title">
+                <span class="iconfont_icon icon_dark">
+                  <svg class="iconfont" aria-hidden="true">
+                    <use
+                      :xlink:href="IsFirstActive ? '#icon-icon_dropdown_b' : '#icon-icon_up_b'"
+                    ></use>
+                  </svg>
+                </span>
+                <span class="stars_red">*</span>Select Time Type
+              </div>
+            </template>
+            <div>
+              <ETDShipments
+                Title="Ocean Shipments"
+                ref="OceanETD"
+                :ETDData="OceanETDInit"
+                @ChangeCheckRules="ChangeETDOceanRules"
+              ></ETDShipments>
+            </div>
+            <div>
+              <ETDShipments
+                Title="Air Shipments"
+                ref="AirETD"
+                :ETDData="AirETDInit"
+                @ChangeCheckRules="ChangeETDAirRules"
+              ></ETDShipments>
+            </div>
+          </el-collapse-item>
+        </el-collapse>
+        <el-collapse
+          style="margin-top: 17px; margin-right: 16px"
+          v-model="RulesActive"
+          @change="IsTwoActive = !IsTwoActive"
+        >
+          <el-collapse-item name="NotificationFrequency">
+            <template #title>
+              <div class="Rules_Title">
+                <span class="iconfont_icon icon_dark">
+                  <svg class="iconfont" aria-hidden="true">
+                    <use
+                      :xlink:href="IsTwoActive ? '#icon-icon_dropdown_b' : '#icon-icon_up_b'"
+                    ></use>
+                  </svg>
+                </span>
+                <span class="stars_red">*</span>Notification Frequency
+              </div>
+            </template>
+            <NotiFrequency
+              v-if="props.TitleType == 'Milestone'"
+              ref="NotiFrequencyDeleteMil"
+              :FrequencyData="FrequencyDataMil"
+              @ChangeFrequencyAdd="ChangeMilFrequency"
+            ></NotiFrequency>
+            <NotiFrequency
+              v-if="props.TitleType == 'Container'"
+              ref="NotiFrequencyDeleteCon"
+              :FrequencyData="FrequencyDataCon"
+              @ChangeFrequencyAdd="ChangeConFrequency"
+            ></NotiFrequency>
+            <NotiFrequency
+              v-if="props.TitleType == 'Departure'"
+              ref="NotiFrequencyDeleteDep"
+              :FrequencyData="FrequencyDataDep"
+              @ChangeFrequencyAdd="ChangeDepFrequency"
+            ></NotiFrequency>
+            <NotiFrequency
+              v-if="props.TitleType == 'ETDChange'"
+              ref="NotiFrequencyDeleteETD"
+              :FrequencyData="FrequencyDataETD"
+              @ChangeFrequencyAdd="ChangeETDFrequency"
+            ></NotiFrequency>
+          </el-collapse-item>
+        </el-collapse>
+        <el-collapse
+          style="margin: 17px 0"
+          v-model="RulesActive"
+          @change="IsThreeActive = !IsThreeActive"
+        >
+          <el-collapse-item name="NotificationMethod">
+            <template #title>
+              <div class="Rules_Title">
+                <span class="iconfont_icon icon_dark">
+                  <svg class="iconfont" aria-hidden="true">
+                    <use
+                      :xlink:href="IsThreeActive ? '#icon-icon_dropdown_b' : '#icon-icon_up_b'"
+                    ></use>
+                  </svg>
+                </span>
+                <span class="stars_red">*</span>Notification Method
+              </div>
+            </template>
+            <NotiMethods
+              v-if="props.TitleType == 'Milestone'"
+              :MethodsData="MethodsDataMil"
+              ref="NotimethodsMil"
+              @ChangeMethodsAdd="ChangeMethodsAddMil"
+            ></NotiMethods>
+            <NotiMethods
+              v-if="props.TitleType == 'Container'"
+              :MethodsData="MethodsDataCon"
+              ref="NotimethodsCon"
+              @ChangeMethodsAdd="ChangeMethodsAddCon"
+            ></NotiMethods>
+            <NotiMethods
+              v-if="props.TitleType == 'Departure'"
+              :MethodsData="MethodsDataDep"
+              ref="NotimethodsDep"
+              @ChangeMethodsAdd="ChangeMethodsAddDep"
+            ></NotiMethods>
+            <NotiMethods
+              v-if="props.TitleType == 'ETDChange'"
+              :MethodsData="MethodsDataETD"
+              ref="NotimethodsETD"
+              @ChangeMethodsAdd="ChangeMethodsAddETD"
+            ></NotiMethods>
+          </el-collapse-item>
+        </el-collapse>
+      </div>
+    </div>
+    <div class="Rules_right">
+      <div class="right_Title">Added Rules</div>
+      <AddedrluesTag
+        :CheckedList="DelayedDeparturedList"
+        v-if="props.TitleType == 'Departure'"
+        @handleCloseRadio="handleCloseDelayed"
+        Title="Ocean Shipments"
+      ></AddedrluesTag>
+      <AddedrluesTag
+        v-if="props.TitleType == 'Milestone'"
+        :CheckedList="OceanCheckList"
+        @handleCloseRadio="handleCloseMilestoneOcean"
+        Title="Ocean Shipments"
+      ></AddedrluesTag>
+      <AddedrluesTag
+        v-if="props.TitleType == 'Container'"
+        :CheckedList="ContainerOceanList"
+        @handleCloseRadio="handleCloseContainer"
+        Title="Ocean Shipments"
+      ></AddedrluesTag>
+      <AddedrluesTag
+        :CheckedList="ETDOceanList"
+        v-if="props.TitleType == 'ETDChange'"
+        Title="Ocean Shipments"
+        @handleCloseRadio="closeOceanETD"
+      ></AddedrluesTag>
+      <AddedrluesTag
+        :CheckedList="ETDAirList"
+        v-if="props.TitleType == 'ETDChange'"
+        @handleCloseRadio="closeAirETD"
+        Title="Air Shipments"
+      ></AddedrluesTag>
+      <AddedrluesTag
+        :CheckedList="DelayedAirdList"
+        v-if="props.TitleType == 'Departure'"
+        @handleCloseRadio="handleCloseAirDelayed"
+        Title="Air Shipments"
+      ></AddedrluesTag>
+      <AddedrluesTag
+        v-if="props.TitleType == 'Milestone'"
+        @handleCloseRadio="handleCloseMilestoneAir"
+        :CheckedList="AirCheckList"
+        Title="Air Shipments"
+      ></AddedrluesTag>
+      <AddedrluesTag
+        v-if="props.TitleType == 'Milestone'"
+        :CheckedList="MilFrequencyList"
+        Title="Notification Frequency"
+        @handleCloseRadio="handleCloseRadio('Mil')"
+      ></AddedrluesTag>
+      <AddedrluesTag
+        v-if="props.TitleType == 'Container'"
+        :CheckedList="ConFrequencyList"
+        Title="Notification Frequency"
+        @handleCloseRadio="handleCloseRadio('Con')"
+      ></AddedrluesTag>
+      <AddedrluesTag
+        v-if="props.TitleType == 'Departure'"
+        :CheckedList="DepFrequencyList"
+        Title="Notification Frequency"
+        @handleCloseRadio="handleCloseRadio('Dep')"
+      ></AddedrluesTag>
+      <AddedrluesTag
+        v-if="props.TitleType == 'ETDChange'"
+        :CheckedList="ETDFrequencyList"
+        Title="Notification Frequency"
+        @handleCloseRadio="handleCloseRadio('ETD')"
+      ></AddedrluesTag>
+      <AddedrluesTag
+        v-if="props.TitleType == 'Milestone'"
+        :CheckedList="MilMethodsList"
+        Title="Notification Method"
+      ></AddedrluesTag>
+      <AddedrluesTag
+        v-if="props.TitleType == 'Container'"
+        :CheckedList="ConMethodsList"
+        Title="Notification Method"
+      ></AddedrluesTag>
+      <AddedrluesTag
+        v-if="props.TitleType == 'Departure'"
+        :CheckedList="DepMethodsList"
+        Title="Notification Method"
+      ></AddedrluesTag>
+      <AddedrluesTag
+        v-if="props.TitleType == 'ETDChange'"
+        :CheckedList="ETDMethodsList"
+        Title="Notification Method"
+      ></AddedrluesTag>
+    </div>
+  </div>
+  <div class="Rules_buttom">
+    <el-button class="el-button--dark rules_button" @click="Savesubscribe">Save</el-button>
+    <el-button @click="CancelRulesVisible = true" class="rules_button" type="default"
+      >Cancel</el-button
+    >
+    <!-- 取消保存 -->
+    <el-dialog v-model="CancelRulesVisible" width="480">
+      <div>You have unsaved changes.</div>
+      <div>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="UnsavedCollapse" 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="UnableSaveVisible" width="480">
+      <div>{{ missingmessage }} missing.</div>
+      <div>Please complete all required fields.</div>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button
+            class="el-button--danger"
+            @click="UnableSaveVisible = 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="SaveedVisible" width="320" style="height: 212px">
+      <div style="text-align: center"><el-image :src="submitsucessful" style="width: 64px;" /></div>
+      <div style="text-align: center; margin-top: 20px">Saved successfully</div>
+    </el-dialog>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.Rules_flex {
+  padding: 0 0 0 8px;
+  display: flex;
+}
+.Rules_left {
+  width: 60%;
+  border-right: 1px solid var(--color-user-config-title-bottom-border);
+}
+.Rules_right {
+  width: 40%;
+  background-color: var(--add-rules-background-color);
+}
+.right_Title {
+  color: var(--color-neutral-1);
+  padding: 9px 8px;
+  font-size: 18px;
+  font-weight: 700;
+  border-bottom: 1px solid var(--color-user-config-title-bottom-border);
+  background-color: var(--color-drawer-body-bg);
+}
+.iconfont {
+  width: 16px;
+  height: 16px;
+  margin-right: 8px;
+}
+.icon_danger {
+  fill: var(--color-btn-danger-bg);
+}
+:deep(.Rules_Title) {
+  color: var(--color-neutral-1);
+  font-size: 14px;
+  font-weight: 700;
+  height: 22px;
+  display: flex;
+  align-items: center;
+}
+.stars_red {
+  color: var(--color-danger);
+}
+:deep(.el-collapse-item__header) {
+  height: 25px !important;
+  margin: 14px 0 0 0 !important;
+  border: none !important;
+  padding: 0 !important;
+}
+.Ocean_collapse {
+  :deep(.el-collapse-item__header) {
+    margin: 4px 0 0 0 !important;
+  }
+}
+:deep(.el-collapse-item__header):hover {
+  background-color: var(--color-system-color-bg) !important;
+  border: none !important;
+}
+:deep(.el-collapse-item__arrow) {
+  width: 0 !important;
+  height: 0 !important;
+}
+:deep(.Ocean_collapse .el-icon svg) {
+  width: 0 !important;
+}
+:deep(.arrivalselect .el-icon svg) {
+  width: 1em !important;
+}
+:deep(.el-collapse-item__header.is-active) {
+  background-color: transparent !important;
+  border-color: transparent !important;
+}
+:deep(.el-collapse-item__arrow.is-active) {
+  transform: rotate(-180deg) !important;
+}
+:deep(.Ocean_collapse .el-collapse-item__arrow) {
+  width: 16px !important;
+  height: 16px !important;
+  background-image: url('../src/images/icon_expand.png') !important;
+  background-size: contain;
+  background-repeat: no-repeat;
+  transform: rotate(0);
+}
+:deep(.Ocean_collapse .el-collapse-item__header) {
+  background-color: var(--color-system-color-bg) !important;
+  padding: 0 8px !important;
+  height: 40px !important;
+}
+:deep(.Ocean_collapse .el-collapse-item) {
+  background-color: var(--color-system-color-bg);
+  border-radius: 12px;
+}
+:deep(.Ocean_collapse .el-collapse-item__wrap) {
+  padding: 0 8px !important;
+}
+:deep(.Ocean_collapse .el-collapse-item__header.is-active) {
+  background-color: var(--color-system-color-bg) !important;
+}
+:deep(.el-checkbox__input.is-checked + .el-checkbox__label) {
+  color: var(--color-neutral-1);
+}
+.Rules_buttom {
+  padding: 8px;
+  border-top: 1px solid var(--color-system-border-1);
+}
+.rules_button {
+  width: 100px;
+  height: 40px;
+}
+.cancel_header {
+  font-size: 18px;
+  font-weight: 700;
+  color: var(--color-neutral-1);
+  display: flex;
+  align-items: center;
+}
+.icon_warning {
+  width: 22px;
+  height: 22px;
+  margin-right: 0;
+  fill: var(--color-btn-warning-bg);
+}
+.iconfont_warning {
+  display: flex;
+  align-items: center;
+}
+:deep(header.el-dialog__header) {
+  background-color: var(--color-system-body-bg);
+}
+:deep(footer.el-dialog__footer) {
+  border-top: none;
+}
+:deep(.el-collapse) {
+  margin-right: 8px;
+}
+</style>

+ 79 - 0
src/components/AddRules/src/components/AddedrluesTag.vue

@@ -0,0 +1,79 @@
+<script setup lang="ts" >
+import { ref, watch } from 'vue'
+const props = defineProps({
+  CheckedList: Array,
+  Title: String
+})
+const CheckedList = ref(props.CheckedList)
+watch(
+  () => props.CheckedList,
+  (current) => {
+    CheckedList.value = current
+  }
+)
+
+const emit = defineEmits(['handleCloseRadio'])
+
+const handleClose = (tag: any) => {
+  CheckedList.value?.splice(CheckedList.value.indexOf(tag), 1)
+  emit('handleCloseRadio', tag)
+}
+</script>
+
+<template>
+  <div>
+    <el-card class="Rules_Card" shadow="never">
+      <div class="Card_Title">{{ props.Title }}</div>
+      <div class="right_flex">
+        <el-tag
+          class="tag"
+          v-for="(tag, index) in CheckedList"
+          :key="index"
+          closable
+          @close="handleClose(tag)"
+        >
+          {{ tag }}
+        </el-tag>
+      </div>
+    </el-card>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.tag {
+  border-radius: 3px;
+  margin-bottom: 4px;
+  color: var(--color-neutral-1);
+  height: 32px;
+  width: fit-content;
+  font-weight: 400;
+  font-size: 14px;
+  background-color: var(--tag-bg-color) !important;
+  border-color: var(--tag-bg-color);
+}
+.tag:last-child {
+  margin-bottom: 0;
+}
+:deep(.el-card__body) {
+  padding: 8px !important;
+  max-height: 400px;
+  overflow-y: scroll;
+  background-color: var(--color-system-card-bg);
+}
+:deep(.el-tag .el-tag__close svg) {
+  width: 16px !important;
+}
+.right_flex {
+  display: flex;
+  flex-direction: column;
+}
+.Rules_Card {
+  margin: 8px;
+}
+.Card_Title {
+  color: var(--color-neutral-1);
+  font-size: 14px;
+  font-weight: 600;
+  margin: 9px 0 11px 0;
+}
+</style>

+ 372 - 0
src/components/AddRules/src/components/DelayedType.vue

@@ -0,0 +1,372 @@
+<script lang="ts" setup>
+import { ref, computed, watch } from 'vue'
+
+const props = defineProps({
+  Title: String,
+  DelayedData: Object
+})
+const delayed_data = ref(props.DelayedData)
+watch(
+  () => props.DelayedData,
+  (current) => {
+    delayed_data.value = current
+    DelayedInit()
+  }
+)
+
+const DelayedInit = () => {
+  let array2: any = []
+  OceanCheckedList.value = []
+  if (delayed_data.value?.atd_etd != '' && delayed_data.value?.atd_etd != undefined) {
+    isDeparture.value = true
+    DepartureTime.value = delayed_data.value?.atd_etd
+    DepartureSelect.value = delayed_data.value?.atd_etd_unit
+    OceanCheckedList.value.push('Departure Delayed')
+    array2.push('Departure')
+  }
+  if (delayed_data.value?.ata_eta != '' && delayed_data.value?.ata_eta != undefined) {
+    isArrival.value = true
+    ArrivalTime.value = delayed_data.value?.ata_eta
+    ArrivalSelect.value = delayed_data.value?.ata_eta_unit
+    OceanCheckedList.value.push('Arrival Delayed (ATA-ETA)')
+    array2.push('Arrival')
+  }
+  CheckChange(OceanCheckedList.value)
+  changedeparture(array2)
+}
+const isDeparture = ref(false)
+const isArrival = ref(false)
+const OceanActive = ref(['DelayedShipments'])
+const OceanCheckedList = ref()
+const DepartureTime = ref()
+const DepartureSelect = ref('')
+const ArrivalTime = ref()
+const ArrivalSelect = ref('')
+const emit = defineEmits(['ChangeCheckRules', 'closeDelayed'])
+const DepartureList = ref({
+  Departure: '',
+  Arrival: ''
+})
+let Departurestr: any = ''
+let Arrivalstr: any = ''
+
+const clampedValue = computed({
+  get: () => DepartureTime.value,
+  set: (newVal) => {
+    const isValid = /^[1-9]\d*$/.test(newVal);
+    DepartureTime.value = isValid ? newVal : '';
+    if( DepartureTime.value!='') {
+      DepartureTime.value = Math.min(Math.max(parseInt(DepartureTime.value, 10), 1), 365)
+    }
+  }
+})
+const clampedArrivalValue = computed({
+  get: () => ArrivalTime.value,
+  set: (newVal) => {
+    const isValid = /^[1-9]\d*$/.test(newVal);
+    ArrivalTime.value = isValid ? newVal : '';
+    if( ArrivalTime.value!='') {
+      ArrivalTime.value = Math.min(Math.max(parseInt(ArrivalTime.value, 10), 1), 365)
+    }
+  }
+})
+const CheckChange = (val: any) => {
+  if (val.includes('Departure Delayed')) {
+    isDeparture.value = true
+    if(clampedValue.value != '' && clampedValue.value!= undefined) {
+      Departurestr = 'Departure Delayed (ATD-ETD)' + ' ≥ ' + clampedValue.value + ' ' + DepartureSelect.value
+    } else {
+      Departurestr = ''
+    }
+    if (DepartureSelect.value != '') {
+      DepartureList.value.Departure = Departurestr
+    }
+    if (val.includes('Arrival Delayed (ATA-ETA)')) {
+      isArrival.value = true
+      if(clampedArrivalValue.value != '' && clampedArrivalValue.value!= undefined) {
+        Arrivalstr = 'Arrival Delayed (ATA-ETA)' + ' ≥ ' + clampedArrivalValue.value + ' ' + ArrivalSelect.value
+      } else {
+        Arrivalstr = ''
+      }
+      if (ArrivalSelect.value != '') {
+        DepartureList.value.Arrival = Arrivalstr
+      }
+    } else {
+      isArrival.value = false
+      DepartureList.value.Arrival = ''
+      clampedArrivalValue.value = ''
+      ArrivalSelect.value = ''
+    }
+  } else {
+    isDeparture.value = false
+    DepartureList.value.Departure = ''
+    clampedValue.value = ''
+    DepartureSelect.value = ''
+    if (val.includes('Arrival Delayed (ATA-ETA)')) {
+      isArrival.value = true
+      if(clampedArrivalValue.value != '' && clampedArrivalValue.value!= undefined) {
+        Arrivalstr = 'Arrival Delayed (ATA-ETA)' + ' ≥ ' + clampedArrivalValue.value + ' ' + ArrivalSelect.value
+      } else {
+        Arrivalstr = ''
+      }
+      if (ArrivalSelect.value != '') {
+        DepartureList.value.Arrival = Arrivalstr
+      }
+    } else {
+      isArrival.value = false
+      DepartureList.value.Arrival = ''
+      clampedArrivalValue.value = ''
+      ArrivalSelect.value = ''
+    }
+  }
+  emit('ChangeCheckRules', DepartureList.value)
+}
+const handleCheckboxClick = (event:any) => {
+  // 判断点击的是否为复选框输入区域
+  const isCheckboxInput = event.target.closest('.el-checkbox__inner')
+  const isCheckboxTitle = event.target.closest('.titlecheckbox')
+  if (!isCheckboxInput) {
+    // 阻止默认切换行为
+    if(!isCheckboxTitle) {
+      event.preventDefault()
+    }
+  }
+}
+const changedeparture = (val: any) => {
+  if(props.Title != 'Air Shipments') {
+    DepartureSelect.value = 'Day(s)'
+    ArrivalSelect.value = 'Day(s)'
+  }
+  if (val == 'Departure') {
+    if(clampedValue.value != '' && clampedValue.value!= undefined) {
+      Departurestr = 'Departure Delayed (ATD-ETD)' + ' ≥ ' + clampedValue.value + ' ' + DepartureSelect.value
+    } else {
+      Departurestr = ''
+    }
+    if (DepartureSelect.value != '') {
+      DepartureList.value.Departure = Departurestr
+    }
+    if (val == 'Arrival') {
+      if(clampedArrivalValue.value != '' && clampedArrivalValue.value!= undefined) {
+        Arrivalstr = 'Arrival Delayed (ATA-ETA)' + ' ≥ ' + clampedArrivalValue.value + ' ' + ArrivalSelect.value
+      } else {
+        Arrivalstr = ''
+      }
+      if (ArrivalSelect.value != '') {
+        DepartureList.value.Arrival = Arrivalstr
+      }
+    }
+  } else {
+    if (val == 'Arrival') {
+      if(clampedArrivalValue.value != '' && clampedArrivalValue.value!= undefined) {
+        Arrivalstr = 'Arrival Delayed (ATA-ETA)' + ' ≥ ' + clampedArrivalValue.value + ' ' + ArrivalSelect.value
+      } else {
+        Arrivalstr = ''
+      }
+      if (ArrivalSelect.value != '') {
+        DepartureList.value.Arrival = Arrivalstr
+      }
+    }
+  }
+  emit('ChangeCheckRules', DepartureList.value)
+}
+const closeDelayed = (val: any) => {
+  if (val.includes('Departure')) {
+    OceanCheckedList.value.splice(OceanCheckedList.value.indexOf('Departure Delayed (ATD-ETD)'), 1)
+    DepartureList.value.Departure = ''
+    isDeparture.value = false
+    DepartureTime.value = ''
+    DepartureSelect.value = ''
+  }
+  if (val.includes('Arrival')) {
+    OceanCheckedList.value.splice(OceanCheckedList.value.indexOf('Arrival Delayed (ATA-ETA)'), 1)
+    DepartureList.value.Arrival = ''
+    isArrival.value = false
+    ArrivalTime.value = ''
+    ArrivalSelect.value = ''
+  }
+  emit('ChangeCheckRules', DepartureList.value)
+}
+
+// 清除所有数据
+const ClearData = () => {
+  OceanCheckedList.value = []
+  DepartureList.value.Departure = ''
+  isDeparture.value = false
+  DepartureList.value.Arrival = ''
+  isArrival.value = false
+  DepartureTime.value = ''
+  DepartureSelect.value = ''
+  ArrivalTime.value = ''
+  ArrivalSelect.value = ''
+}
+
+defineExpose({
+  closeDelayed,
+  ClearData
+})
+</script>
+<template>
+  <div class="Ocean_collapse">
+    <el-collapse v-model="OceanActive">
+      <el-collapse-item name="DelayedShipments">
+        <template #title>
+          <div class="Rules_Title OceanTitle">{{ props.Title }}</div>
+        </template>
+        <div class="oceanCheckbox">
+          <el-checkbox-group @change="CheckChange" v-model="OceanCheckedList">
+            <el-checkbox @click="handleCheckboxClick($event)" class="delayedType" value="Departure Delayed">
+              <div class="titlecheckbox">Departure Delayed (ATD-ETD)</div>
+              <div v-if="isDeparture" class="flex" style="margin-top: 16px">
+                <span class="delayedTitle">Delayed Time</span>
+                <span class="delayedIcon">≥</span>
+                <el-input
+                  v-model="clampedValue"
+                  class="input-with-select"
+                  @input="changedeparture('Departure')"
+                  v-if="props.Title == 'Air Shipments'" 
+                >
+                  <template #append>
+                    <el-select
+                      v-model="DepartureSelect"
+                      placeholder="Select"
+                      class="arrivalselect"
+                      @change="changedeparture('Departure')"
+                    >
+                      <el-option label="Day(s)" value="Day(s)" />
+                      <el-option label="Hour(s)" value="Hour(s)" />
+                    </el-select>
+                  </template>
+                </el-input>
+                <el-input
+                  v-else
+                  v-model="clampedValue"
+                  @input="changedeparture('Departure')"
+                  class="input-with-select1"
+                >
+                </el-input>
+                <div
+                v-if="props.Title != 'Air Shipments'" class="Days">Day(s)</div>
+              </div>
+            </el-checkbox>
+            <el-checkbox class="delayedType" @click="handleCheckboxClick($event)"  value="Arrival Delayed (ATA-ETA)">
+              <div class="titlecheckbox">Arrival Delayed (ATA-ETA)</div>
+              <div v-if="isArrival" class="flex" style="margin-top: 16px">
+                <span class="delayedTitle">Delayed Time</span>
+                <span class="delayedIcon">≥</span>
+                <el-input
+                  v-model="clampedArrivalValue"
+                  @input="changedeparture('Arrival')"
+                  class="input-with-select"
+                  v-if="props.Title == 'Air Shipments'"
+                >
+                  <template #append>
+                    <el-select
+                      v-model="ArrivalSelect"
+                      placeholder="Select"
+                      class="arrivalselect"
+                      @change="changedeparture('Arrival')"
+                    >
+                      <el-option label="Day(s)" value="Day(s)" />
+                      <el-option  label="Hour(s)" value="Hour(s)" />
+                    </el-select>
+                  </template>
+                </el-input>
+                <el-input
+                  v-else
+                  v-model="clampedArrivalValue"
+                  @input="changedeparture('Arrival')"
+                  class="input-with-select1"
+                >
+                </el-input>
+                <div v-if="props.Title != 'Air Shipments'" class="Days">Day(s)</div>
+              </div>
+            </el-checkbox>
+          </el-checkbox-group>
+        </div>
+      </el-collapse-item>
+    </el-collapse>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+:deep(.el-checkbox-group) {
+  border: 1px solid var(--color-system-border);
+  border-radius: 5px;
+}
+:deep(.el-checkbox) {
+  width: 100%;
+  background-color: var(--color-system-body-bg);
+  border-bottom: 1px solid var(--color-system-border);
+  padding: 8px;
+}
+:deep(.el-checkbox:first-child) {
+  border-radius: 6px 6px 0 0;
+}
+:deep(.el-checkbox:last-child) {
+  border-radius: 0 0 6px 6px;
+  border-bottom: none;
+}
+.delayedType {
+  align-items: start;
+  height: fit-content;
+  margin-right: 5px;
+  border-radius: 6px;
+  padding: 8px;
+}
+.input-with-select {
+  width: 180px;
+}
+.input-with-select1 {
+  width: 84px;
+  border-radius: 0;
+}
+:deep(.el-input__wrapper) {
+  width: 50%;
+  border-radius: 6px 0 0 6px;
+}
+:deep(.el-input-group__append) {
+  width: 50%;
+  padding: 0;
+  border: none;
+}
+:deep(.el-input-group--append .el-input-group__append .el-select .el-select__wrapper) {
+  padding: 5px 10px;
+  background-color: var(--color-system-body-bg);
+}
+.delayedTitle {
+  color: var(--color-neutral-2);
+  font-size: 12px;
+}
+.delayedIcon {
+  margin: 0 8px;
+}
+.oceanCheckbox {
+  margin-bottom: 8px;
+}
+:deep(.el-input__wrapper) {
+  box-shadow: 0 0 0 1px var(--color-system-border-1) inset;
+}
+:deep(.el-input-group--append .el-input-group__append .el-select .el-select__wrapper) {
+  box-shadow: 0 1px 0 0 var(--color-system-border-1) inset,0 -1px 0 0 var(--color-system-border-1) inset,-1px 0 0 0 var(--color-system-border-1) inset;
+}
+.Days {
+  width: 84px;
+  border: 1px solid var(--color-system-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: 30px;
+}
+.flex {
+  display: flex;
+  align-items: center;
+}
+.titlecheckbox {
+  max-width: 198px;
+}
+</style>

+ 503 - 0
src/components/AddRules/src/components/ETDShipments.vue

@@ -0,0 +1,503 @@
+<script lang="ts" setup>
+import { ref, computed, watch } from 'vue'
+
+const props = defineProps({
+  Title: String,
+  ETDData: Object
+})
+const ETD_data = ref(props.ETDData)
+watch(
+  () => props.ETDData,
+  (current) => {
+    ETD_data.value = current
+    ETDInit()
+  }
+)
+
+const ETDInit = () => {
+  OceanCheckedList.value = []
+  if (ETD_data.value?.ETDradio == 't') {
+    ETDRadio.value = '1'
+    OceanCheckedList.value.push('ETD')
+  } else {
+    if (ETD_data.value?.etd_old_sub_new != '' && ETD_data.value?.etd_old_sub_new != undefined) {
+      ETDRadio.value = '2'
+      isETD.value = true
+      ETDTime.value = ETD_data.value?.etd_old_sub_new
+      ETDSelect.value = ETD_data.value?.etd_old_sub_new_unit
+      OceanCheckedList.value.push('ETD')
+    }
+  }
+  if (ETD_data.value?.ETAradio == 't') {
+    ETARadio.value = '1'
+    OceanCheckedList.value.push('ETA')
+  } else {
+    if (ETD_data.value?.eta_old_sub_new != '' && ETD_data.value?.eta_old_sub_new != undefined) {
+      ETARadio.value = '2'
+      isETA.value = true
+      ETATime.value = ETD_data.value?.eta_old_sub_new
+      ETASelect.value = ETD_data.value?.eta_old_sub_new_unit
+      OceanCheckedList.value.push('ETA')
+    }
+  }
+  CheckChange(OceanCheckedList.value)
+  changeETDRadio(ETDRadio.value)
+  changeETARadio(ETARadio.value)
+}
+const isETD = ref(false)
+const isETA = ref(false)
+const OceanActive = ref(['ETDShipments'])
+const OceanCheckedList = ref()
+const ETDTime = ref()
+const ETDSelect = ref('')
+const ETATime = ref()
+const ETASelect = ref('')
+const ETDRadio = ref()
+const ETARadio = ref()
+const emit = defineEmits(['ChangeCheckRules', 'closeETD'])
+const ETDETAList = ref({
+  ETD: '',
+  ETA: ''
+})
+let ETDstr: any = ''
+let ETAstr: any = ''
+const CheckChange = (val: any) => {
+  if(props.Title != 'Air Shipments') {
+    ETASelect.value = 'Day(s)'
+    ETDSelect.value = 'Day(s)'
+  }
+  if (val.includes('ETD')) {
+    isETD.value = true
+    if(ETDstr.includes('all changes')) {
+      ETDETAList.value.ETD = ETDstr
+    } else {
+      if (ETDSelect.value != ''  && clampedValue.value!= '' && clampedValue.value!= undefined) {
+        ETDETAList.value.ETD = ETDstr
+      } else {
+        ETDETAList.value.ETD = ''
+      }
+    }
+    if (val.includes('ETA')) {
+      isETA.value = true
+      if(ETAstr.includes('all changes')) {
+        ETDETAList.value.ETA = ETAstr
+      } else {
+        if (ETASelect.value != ''  && clampedETAValue.value!= '' && clampedETAValue.value!= undefined) {
+          ETDETAList.value.ETA = ETAstr
+        } else {
+          ETDETAList.value.ETA = ''
+        }
+      }
+    } else {
+      isETA.value = false
+      ETDETAList.value.ETA = ''
+      clampedETAValue.value = ''
+      ETARadio.value = ''
+      ETASelect.value = ''
+      ETAstr = ''
+    }
+  } else {
+    isETD.value = false
+    ETDETAList.value.ETD = ''
+    clampedValue.value = ''
+    ETDRadio.value = ''
+    ETDSelect.value = ''
+    ETDstr = ''
+    if (val.includes('ETA')) {
+      isETA.value = true
+      if(ETAstr.includes('all changes')) {
+        ETDETAList.value.ETA = ETAstr
+      } else {
+        if (ETASelect.value != ''  && clampedETAValue.value!= '' && clampedETAValue.value!= undefined) {
+          ETDETAList.value.ETA = ETAstr
+        } else {
+          ETDETAList.value.ETA = ''
+        }
+      }
+    } else {
+      isETA.value = false
+      ETDETAList.value.ETA = ''
+      clampedETAValue.value = ''
+      ETARadio.value = ''
+      ETASelect.value = ''
+      ETAstr = ''
+    }
+  }
+  emit('ChangeCheckRules', ETDETAList.value)
+}
+const changeETDRadio = (val: any) => {
+  if(props.Title != 'Air Shipments') {
+    ETDSelect.value = 'Day(s)'
+  }
+  if (val == 1) {
+    clampedValue.value = ''
+    ETDstr = 'ETD: Notify for all changes'
+    ETDETAList.value.ETD = ETDstr
+  } else if (val == 2) {
+    ETDstr = 'ETD: Notify only when time difference ≥ ' + clampedValue.value + ' ' + ETDSelect.value
+    if (ETDSelect.value != '' && clampedValue.value!= '' && clampedValue.value!= undefined) {
+      ETDETAList.value.ETD = ETDstr
+    } else {
+      ETDETAList.value.ETD = ''
+    }
+  }
+  emit('ChangeCheckRules', ETDETAList.value)
+}
+const changeETARadio = (val: any) => {
+  if(props.Title != 'Air Shipments') {
+    ETASelect.value = 'Day(s)'
+  }
+  if (val == 1) {
+    ETAstr = 'ETA: Notify for all changes'
+    ETDETAList.value.ETA = ETAstr
+    clampedETAValue.value = ''
+  } else if (val == 2) {
+    ETAstr = 'ETA: Notify only when time difference ≥ ' + clampedETAValue.value + ' ' + ETASelect.value
+    if (ETASelect.value != '' && clampedETAValue.value!= '' && clampedETAValue.value!= undefined) {
+      ETDETAList.value.ETA = ETAstr
+    } else {
+      ETDETAList.value.ETA = ''
+    }
+  }
+  emit('ChangeCheckRules', ETDETAList.value)
+}
+const changedeparture = (val: any) => {
+  if(props.Title != 'Air Shipments') {
+    ETDSelect.value = 'Day(s)'
+    ETASelect.value = 'Day(s)'
+  }
+  if (val == 'ETD') {
+    // if(ETDstr.includes('all changes')) {
+    //   ETDETAList.value.ETD = ETDstr
+    // } else {
+    //   ETDstr = 'ETD: Notify only when time difference ≥ ' + clampedValue.value + ' ' + ETDSelect.value
+    //   if (ETDSelect.value != '' && clampedValue.value!= '' && clampedValue.value!= undefined) {
+    //     ETDRadio.value = '2'
+    //     ETDETAList.value.ETD = ETDstr
+    //   } else {
+    //     ETDETAList.value.ETD = ''
+    //   }
+    ETDstr = 'ETD: Notify only when time difference ≥ ' + clampedValue.value + ' ' + ETDSelect.value
+    ETDRadio.value = '2'
+    if (ETDSelect.value != '' && clampedValue.value!= '' && clampedValue.value!= undefined) {
+      ETDETAList.value.ETD = ETDstr
+    } else {
+      ETDETAList.value.ETD = ''
+    }
+    if (val == 'ETA') {
+      // if(ETAstr.includes('all changes')) {
+      //   ETDETAList.value.ETA = ETAstr
+      // } else {
+      //   ETAstr = 'ETA: Notify only when time difference ≥ ' + clampedETAValue.value + ' ' + ETASelect.value
+      //   if (ETASelect.value != '' && clampedETAValue.value!= '' && clampedETAValue.value!= undefined) {
+      //     ETARadio.value = '2'
+      //     ETDETAList.value.ETA = ETAstr
+      //   } else {
+      //     ETDETAList.value.ETA = ''
+      //   }
+      // }
+      ETAstr = 'ETA: Notify only when time difference ≥ ' + clampedETAValue.value + ' ' + ETASelect.value
+      ETARadio.value = '2'
+      if (ETASelect.value != '' && clampedETAValue.value!= '' && clampedETAValue.value!= undefined) {
+        ETDETAList.value.ETA = ETAstr
+      } else {
+        ETDETAList.value.ETA = ''
+      }
+    }
+  } else {
+    // if(ETAstr.includes('all changes')) {
+    //   ETDETAList.value.ETA = ETAstr
+    // } else {
+    //   ETAstr = 'ETA: Notify only when time difference ≥ ' + clampedETAValue.value + ' ' + ETASelect.value
+    //   if (ETASelect.value != '' && clampedETAValue.value!= '' && clampedETAValue.value!= undefined) {
+    //     ETARadio.value = '2'
+    //     ETDETAList.value.ETA = ETAstr
+    //   } else {
+    //     ETDETAList.value.ETA = ''
+    //   }
+    // }
+    ETAstr = 'ETA: Notify only when time difference ≥ ' + clampedETAValue.value + ' ' + ETASelect.value
+    ETARadio.value = '2'
+    if (ETASelect.value != '' && clampedETAValue.value!= '' && clampedETAValue.value!= undefined) {
+      ETDETAList.value.ETA = ETAstr
+    } else {
+      ETDETAList.value.ETA = ''
+    }
+  }
+  emit('ChangeCheckRules', ETDETAList.value)
+}
+const handleCheckboxClick = (event:any) => {
+  // 判断点击的是否为复选框输入区域
+  const isCheckboxInput = event.target.closest('.el-checkbox__inner')
+  const isCheckboxTitle = event.target.closest('.titlecheckbox')
+  const isCheckboxRadio = event.target.closest('.radiocheckbox')
+  if (!isCheckboxInput) {
+    // 阻止默认切换行为
+    if(!isCheckboxTitle) {
+      if(!isCheckboxRadio) {
+        event.preventDefault()
+      }
+    }
+  }
+}
+const closeETD = (val: any) => {
+  if (val.includes('ETD')) {
+    OceanCheckedList.value.splice(OceanCheckedList.value.indexOf('ETD'), 1)
+    ETDETAList.value.ETD = ''
+    isETD.value = false
+    ETDTime.value = ''
+    ETDSelect.value = ''
+    ETDRadio.value = 0
+  }
+  if (val.includes('ETA')) {
+    OceanCheckedList.value.splice(OceanCheckedList.value.indexOf('ETA'), 1)
+    ETDETAList.value.ETA = ''
+    isETA.value = false
+    ETATime.value = ''
+    ETASelect.value = ''
+    ETARadio.value = 0
+  }
+  emit('ChangeCheckRules', ETDETAList.value)
+}
+
+// 清除所有数据
+const ClearData = () => {
+  OceanCheckedList.value = []
+  ETDETAList.value.ETD = ''
+  isETD.value = false
+  ETDETAList.value.ETA = ''
+  isETA.value = false
+  ETDRadio.value = 0
+  ETARadio.value = 0
+  ETDTime.value = ''
+  ETDSelect.value = ''
+  ETATime.value = ''
+  ETASelect.value = ''
+}
+
+defineExpose({
+  closeETD,
+  ClearData
+})
+
+const clampedValue = computed({
+  get: () => ETDTime.value,
+  set: (newVal) => {
+    const isValid = /^[1-9]\d*$/.test(newVal);
+    ETDTime.value = isValid ? newVal : '';
+    if( ETDTime.value!='') {
+      ETDTime.value = Math.min(Math.max(parseInt(ETDTime.value, 10), 1), 365)
+    }
+  }
+})
+const clampedETAValue = computed({
+  get: () => ETATime.value,
+  set: (newVal) => {
+    const isValid = /^[1-9]\d*$/.test(newVal);
+    ETATime.value = isValid ? newVal : '';
+    if( ETATime.value!='') {
+      ETATime.value = Math.min(Math.max(parseInt(ETATime.value, 10), 1), 365)
+    }
+  }
+})
+</script>
+<template>
+  <div class="Ocean_collapse">
+    <el-collapse v-model="OceanActive">
+      <el-collapse-item name="ETDShipments">
+        <template #title>
+          <div class="Rules_Title OceanTitle">{{ props.Title }}</div>
+        </template>
+        <div class="oceanCheckbox">
+          <el-checkbox-group @change="CheckChange" v-model="OceanCheckedList">
+            <el-checkbox @click="handleCheckboxClick($event)" class="delayedType" value="ETD">
+              <div class="titlecheckbox">ETD</div>
+              <div v-if="isETD" style="margin-top: 16px">
+                <el-radio-group class="radiocheckbox" v-model="ETDRadio" @change="changeETDRadio">
+                  <el-radio value="1">Notify for all changes</el-radio>
+                  <el-radio value="2">
+                    <div class="flex">
+                      Notify only when time difference
+                      <span class="delayedIcon">≥</span>
+                      <el-input
+                      v-if="props.Title == 'Air Shipments'"
+                        v-model="clampedValue"
+                        class="input-with-select"
+                        @input="changedeparture('ETD')"
+                      >
+                        <template #append>
+                          <el-select
+                            v-model="ETDSelect"
+                            placeholder="Select"
+                            class="arrivalselect"
+                            @change="changedeparture('ETD')"
+                          >
+                            <el-option label="Day(s)" value="Day(s)" />
+                            <el-option label="Hour(s)" value="Hour(s)" />
+                          </el-select>
+                        </template>
+                      </el-input>
+                      <el-input
+                        v-else
+                        v-model="clampedValue"
+                        @input="changedeparture('ETD')"
+                        class="input-with-select1"
+                      >
+                      </el-input>
+                      <div
+                      v-if="props.Title != 'Air Shipments'" class="Days">Day(s)</div>
+                    </div>
+                  </el-radio>
+                </el-radio-group>
+              </div>
+            </el-checkbox>
+            <el-checkbox @click="handleCheckboxClick($event)" class="delayedType" value="ETA">
+              <div class="titlecheckbox">ETA</div>
+              <div v-if="isETA" style="margin-top: 16px">
+                <el-radio-group class="radiocheckbox" v-model="ETARadio" @change="changeETARadio">
+                  <el-radio value="1">Notify for all changes</el-radio>
+                  <el-radio value="2">
+                    <div class="flex">
+                      Notify only when time difference
+                      <span class="delayedIcon">≥</span>
+                      <el-input
+                      v-if="props.Title == 'Air Shipments'"
+                        v-model="clampedETAValue"
+                        class="input-with-select"
+                        @input="changedeparture('ETA')"
+                      >
+                        <template #append>
+                          <el-select
+                            v-model="ETASelect"
+                            placeholder="Select"
+                            class="arrivalselect"
+                            @change="changedeparture('ETA')"
+                          >
+                            <el-option label="Day(s)" value="Day(s)" />
+                            <el-option label="Hour(s)" value="Hour(s)" />
+                          </el-select>
+                        </template>
+                      </el-input>
+                      <el-input
+                        v-else
+                        v-model="clampedETAValue"
+                        @input="changedeparture('ETA')"
+                        class="input-with-select1"
+                      >
+                      </el-input>
+                      <div
+                      v-if="props.Title != 'Air Shipments'" class="Days">Day(s)</div>
+                    </div>
+                  </el-radio>
+                </el-radio-group>
+              </div>
+            </el-checkbox>
+          </el-checkbox-group>
+        </div>
+      </el-collapse-item>
+    </el-collapse>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+:deep(.el-checkbox-group) {
+  border: 1px solid var(--color-system-border);
+  border-radius: 5px;
+}
+:deep(.el-checkbox) {
+  width: 100%;
+  background-color: var(--color-system-body-bg);
+  border-bottom: 1px solid var(--color-system-border);
+  padding: 8px;
+}
+:deep(.el-checkbox:first-child) {
+  border-radius: 6px 6px 0 0;
+}
+:deep(.el-checkbox:last-child) {
+  border-radius: 0 0 6px 6px;
+  border-bottom: none;
+}
+:deep(.el-radio) {
+  border: none !important;
+}
+:deep( .el-radio__inner) {
+  border: 1px solid var(--color-system-checkbox-border);
+}
+.delayedType {
+  align-items: start;
+  height: fit-content;
+  margin-right: 5px;
+  border-radius: 6px;
+  padding: 8px;
+}
+.input-with-select {
+  width: 180px;
+}
+.input-with-select1 {
+  width: 84px;
+  border-radius: 0;
+}
+:deep(.el-input__wrapper) {
+  width: 50%;
+  border-radius: 6px 0 0 6px;
+}
+:deep(.el-input-group__append) {
+  width: 50%;
+  padding: 0;
+  border: none;
+}
+:deep(.el-input-group--append .el-input-group__append .el-select .el-select__wrapper) {
+  padding: 5px 10px;
+  background-color: var(--color-system-body-bg);
+}
+.delayedIcon {
+  margin: 0 8px;
+}
+.oceanCheckbox {
+  margin-bottom: 8px;
+}
+: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;
+  line-height: 32px;
+  align-items: center;
+}
+:deep(.el-radio__input.is-checked + .el-radio__label) {
+  color: var(--color-neutral-1);
+}
+:deep(.el-input__wrapper) {
+  box-shadow: 0 0 0 1px var(--color-system-border-1) inset;
+}
+:deep(.el-input-group--append .el-input-group__append .el-select .el-select__wrapper) {
+  box-shadow: 0 1px 0 0 var(--color-system-border-1) inset,0 -1px 0 0 var(--color-system-border-1) inset,-1px 0 0 0 var(--color-system-border-1) inset;
+}
+.Days {
+  width: 84px;
+  border: 1px solid var(--color-system-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: 30px;
+}
+.flex {
+  display: flex;
+  align-items: center;
+}
+.titlecheckbox {
+  max-width: 28px;
+}
+</style>

+ 415 - 0
src/components/AddRules/src/components/NotiFrequency.vue

@@ -0,0 +1,415 @@
+<script setup lang="ts">
+import { ref, watch } from 'vue'
+import moment from 'moment-timezone'
+
+const props = defineProps({
+  FrequencyData: Object
+})
+
+const frequency_data = ref(props.FrequencyData)
+const radio = ref(0)
+const FrequencyList = ref()
+const DailyTime = ref('')
+const WeeklyTime = ref('')
+const WeeklyDay = ref('')
+const WeekDay = ref([
+  {
+    label: 'Monday',
+    value: 'Monday'
+  },
+  {
+    label: 'Tuesday',
+    value: 'Tuesday'
+  },
+  {
+    label: 'Wednesday',
+    value: 'Wednesday'
+  },
+  {
+    label: 'Thursday',
+    value: 'Thursday'
+  },
+  {
+    label: 'Friday',
+    value: 'Friday'
+  },
+  {
+    label: 'Saturday',
+    value: 'Saturday'
+  },
+  {
+    label: 'Sunday',
+    value: 'Sunday'
+  }
+])
+const TimeZone = 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 TimeZoneDailySelect = ref()
+const TimeZoneWeeklySelect = ref()
+TimeZoneDailySelect.value = 'UTC' + moment().tz(moment.tz.guess()).format('Z')
+let defaultDailyTimeZone = TimeZoneDailySelect.value
+TimeZoneWeeklySelect.value = 'UTC' + moment().tz(moment.tz.guess()).format('Z')
+let defaultWeeklyTimeZone = TimeZoneDailySelect.value
+const isDaily = ref(false)
+const isWeekly = ref(false)
+let savesubscribeobj: any = {}
+
+watch(
+  () => props.FrequencyData,
+  (current) => {
+    frequency_data.value = current
+    FrequencyDataInit()
+  }
+)
+
+const emits = defineEmits(['ChangeFrequencyAdd'])
+
+// 初始设置Frequency
+const FrequencyDataInit = () => {
+  if (frequency_data.value?.frequency_type == 'Instant') {
+    radio.value = 1
+    ChangeFrequency(1)
+  } else if (frequency_data.value?.frequency_type == 'Daily') {
+    radio.value = 2
+    DailyTime.value = frequency_data.value?.daily_time
+    TimeZoneDailySelect.value = frequency_data.value?.daily_time_zone
+    ChangeFrequency(2)
+  } else if (frequency_data.value?.frequency_type == 'Weekly') {
+    radio.value = 3
+    WeeklyDay.value = frequency_data.value?.weekly_week
+    WeeklyTime.value = frequency_data.value?.weekly_time
+    TimeZoneWeeklySelect.value = frequency_data.value?.weekly_time_zone
+    ChangeFrequency(3)
+  } else {
+    radio.value = 0
+    ChangeFrequency(0)
+  }
+}
+
+// 更改Frequency时间
+const ChangeFrequency = (val: any) => {
+  FrequencyList.value = []
+  let str: any = ''
+  if (val == 1) {
+    isDaily.value = false
+    isWeekly.value = false
+    DailyTime.value = ''
+    WeeklyDay.value = ''
+    WeeklyTime.value = ''
+    TimeZoneDailySelect.value  = defaultDailyTimeZone
+    TimeZoneWeeklySelect.value  = defaultWeeklyTimeZone
+    str = 'Instant notification for each update'
+    FrequencyList.value.push(str)
+    savesubscribeobj.frequency_type = 'Instant'
+    savesubscribeobj.frequency_display = str
+  } else if (val == 2) {
+    isDaily.value = true
+    isWeekly.value = false
+    WeeklyDay.value = ''
+    WeeklyTime.value = ''
+    TimeZoneWeeklySelect.value  = defaultWeeklyTimeZone
+    str = 'Daily, ' + DailyTime.value + ', ' + TimeZoneDailySelect.value
+    if (DailyTime.value != '' && DailyTime.value != undefined && TimeZoneDailySelect.value !== '') {
+      FrequencyList.value.push(str)
+    }
+    savesubscribeobj.frequency_type = 'Daily'
+    savesubscribeobj.daily_time = DailyTime.value
+    savesubscribeobj.daily_time_zone = TimeZoneDailySelect.value
+    savesubscribeobj.frequency_display = str
+  } else if (val == 3) {
+    isDaily.value = false
+    isWeekly.value = true
+    DailyTime.value = ''
+    TimeZoneDailySelect.value  = defaultDailyTimeZone
+    str = 'Weekly, ' + WeeklyDay.value + ', ' + WeeklyTime.value + ', ' + TimeZoneWeeklySelect.value
+    if (WeeklyDay.value != '' && WeeklyTime.value != undefined && WeeklyTime.value != '' && TimeZoneWeeklySelect.value !== '') {
+      FrequencyList.value.push(str)
+    }
+    savesubscribeobj.frequency_type = 'Weekly'
+    if (WeeklyDay.value == 'Monday') {
+      savesubscribeobj.weekly_week = 1
+    } else if (WeeklyDay.value == 'Tuesday') {
+      savesubscribeobj.weekly_week = 2
+    } else if (WeeklyDay.value == 'Wednesday') {
+      savesubscribeobj.weekly_week = 3
+    } else if (WeeklyDay.value == 'Thursday') {
+      savesubscribeobj.weekly_week = 4
+    } else if (WeeklyDay.value == 'Friday') {
+      savesubscribeobj.weekly_week = 5
+    } else if (WeeklyDay.value == 'Saturday') {
+      savesubscribeobj.weekly_week = 6
+    } else if (WeeklyDay.value == 'Sunday') {
+      savesubscribeobj.weekly_week = 0
+    }
+    savesubscribeobj.weekly_time = WeeklyTime.value
+    savesubscribeobj.weekly_time_zone = TimeZoneWeeklySelect.value
+    savesubscribeobj.frequency_display = str
+  } else {
+    isDaily.value = false
+    isWeekly.value = false
+    DailyTime.value = ''
+    WeeklyTime.value = ''
+    WeeklyDay.value = ''
+    TimeZoneDailySelect.value  = defaultDailyTimeZone
+    TimeZoneWeeklySelect.value  = defaultWeeklyTimeZone
+    delete savesubscribeobj.frequency_type
+    delete savesubscribeobj.daily_time
+    delete savesubscribeobj.daily_time_zone
+    delete savesubscribeobj.weekly_week
+    delete savesubscribeobj.weekly_time
+    delete savesubscribeobj.weekly_time_zone
+  }
+  emits('ChangeFrequencyAdd', FrequencyList.value, savesubscribeobj)
+}
+const changeTime = (val: any) => {
+  if (val == 'Daily') {
+    ChangeFrequency(2)
+  } else {
+    ChangeFrequency(3)
+  }
+}
+
+// 删除 Frequency tag
+const handleCloseRadioFrequency = () => {
+  radio.value = 0
+  ChangeFrequency(0)
+}
+
+const user_type = localStorage.getItem('user_type')
+
+defineExpose({
+  handleCloseRadioFrequency,
+  FrequencyDataInit
+})
+</script>
+<template>
+  <div style="margin-top: 11px">
+    <el-radio-group v-model="radio" @change="ChangeFrequency">
+      <el-radio :value="1" v-if="user_type != null && user_type != 'customer'"
+        >Instant notification for each update</el-radio
+      >
+      <el-radio :value="2">
+        <div>Daily Summary</div>
+        <div class="Daily" v-if="isDaily">
+          <div class="Daily_left" style="margin-right: 8px">
+            Select Time
+            <div>
+              <el-time-select
+                v-model="DailyTime"
+                start="00:00"
+                step="00:30"
+                end="23:30"
+                prefix-icon=""
+                @change="changeTime('Daily')"
+                placeholder="Select Time"
+              ></el-time-select>
+            </div>
+          </div>
+          <div class="Daily_left">
+            Select Time Zone
+            <div>
+              <el-select
+                v-model="TimeZoneDailySelect"
+                placeholder="Select Time Zone"
+                @change="changeTime('Daily')"
+              >
+                <el-option
+                  v-for="item in TimeZone"
+                  :key="item.value"
+                  :label="item.label"
+                  :value="item.value"
+                />
+              </el-select>
+            </div>
+          </div>
+        </div>
+      </el-radio>
+      <el-radio :value="3">
+        <div>Weekly Summary</div>
+        <div class="Daily" v-if="isWeekly">
+          <div class="Weekly_left">
+            Select Day
+            <div>
+              <el-select
+                v-model="WeeklyDay"
+                @change="changeTime('Weekly')"
+                placeholder="Select Day"
+              >
+                <el-option
+                  v-for="item in WeekDay"
+                  :key="item.value"
+                  :label="item.label"
+                  :value="item.value"
+                />
+              </el-select>
+            </div>
+          </div>
+          <div class="Weekly_left" style="margin: 0 8px">
+            Select Time
+            <div>
+              <el-time-select
+                v-model="WeeklyTime"
+                @change="changeTime('Weekly')"
+                start="00:00"
+                step="00:30"
+                end="23:30"
+                prefix-icon=""
+                placeholder="Select time"
+              ></el-time-select>
+            </div>
+          </div>
+          <div class="Weekly_left">
+            Select Time Zone
+            <div>
+              <el-select
+                v-model="TimeZoneWeeklySelect"
+                placeholder="Select Time Zone"
+                @change="changeTime('Weekly')"
+              >
+                <el-option
+                  v-for="item in TimeZone"
+                  :key="item.value"
+                  :label="item.label"
+                  :value="item.value"
+                />
+              </el-select>
+            </div>
+          </div>
+        </div>
+      </el-radio>
+    </el-radio-group>
+  </div>
+</template>
+<style lang="scss" scoped>
+: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;
+  line-height: 32px;
+  align-items: start;
+}
+:deep(.el-radio__input.is-checked + .el-radio__label) {
+  color: var(--color-neutral-1);
+}
+:deep( .el-radio__inner) {
+  border: 1px solid var(--color-system-checkbox-border);
+}
+.Daily {
+  margin: 0 0 9px 0;
+  display: flex;
+}
+.Daily_left {
+  color: var(--color-neutral-2);
+  width: 50%;
+}
+.Weekly_left {
+  color: var(--color-neutral-2);
+  width: 33%;
+}
+:deep(.el-select__icon.el-icon svg) {
+  width: 1em !important;
+}
+:deep(.el-select__wrapper.is-filterable) {
+  padding-left: 7px;
+}
+:deep(.el-radio__inner) {
+  margin-top: 7px;
+}
+</style>

+ 112 - 0
src/components/AddRules/src/components/NotiMethods.vue

@@ -0,0 +1,112 @@
+<script setup lang="ts">
+import { ref, watch, computed } from 'vue'
+import Light_methods from '../images/illustration_system massage@2x.png'
+import Dark_methods from '../images/illustration_system massage_darkmode@2x.png'
+import { useThemeStore } from '@/stores/modules/theme'
+
+const themeStore = useThemeStore()
+
+const checkMethodList = ref()
+checkMethodList.value = []
+let savesubscribeobj: any = {}
+const props = defineProps({
+  MethodsData: Object
+})
+const MethodsImg = computed(() => {
+  return themeStore.theme === 'dark' ? Dark_methods : Light_methods
+})
+
+const methods_data = ref(props.MethodsData)
+watch(
+  () => props.MethodsData,
+  (current) => {
+    methods_data.value = current
+    MethodsInit()
+  }
+)
+const emits = defineEmits(['ChangeMethodsAdd'])
+// 初始设置Methods
+const MethodsInit = () => {
+  checkMethodList.value = []
+  if (methods_data.value?.method_display != undefined) {
+    if (methods_data.value?.method_display.indexOf('Email') != -1) {
+      checkMethodList.value.push('By Email')
+      savesubscribeobj.method_by_email = true
+    }
+  }
+  if (methods_data.value?.method_display != undefined) {
+    if (methods_data.value?.method_display.indexOf('System Message') != -1) {
+      checkMethodList.value.push('By System Message')
+      savesubscribeobj.method_by_message = true
+    }
+  }
+  changeMethod(checkMethodList.value)
+}
+
+// 选中Method
+const changeMethod = (val: any) => {
+  if (val.indexOf('By Email') != -1) {
+    savesubscribeobj.method_by_email = true
+  } else {
+    savesubscribeobj.method_by_email = false
+  }
+  if (val.indexOf('By System Message') != -1) {
+    savesubscribeobj.method_by_message = true
+  } else {
+    savesubscribeobj.method_by_message = false
+  }
+  if (val.length != 0) {
+    savesubscribeobj.method_display = val.map((e: any) => e.replace('By ', '')).join(',')
+  } else {
+    savesubscribeobj.method_display = ''
+  }
+  emits('ChangeMethodsAdd', checkMethodList.value, savesubscribeobj)
+}
+const user_type = localStorage.getItem('user_type')
+
+// 删除 Frequency tag
+const handleCloseMethods = () => {
+  checkMethodList.value = []
+}
+
+defineExpose({
+  handleCloseMethods
+})
+</script>
+<template>
+  <div style="margin-top: 11px">
+    <div class="Method">
+      <el-checkbox-group v-model="checkMethodList" @change="changeMethod">
+        <el-checkbox
+          class="methodcheckbox"
+          value="By Email"
+          v-if="user_type != null && user_type != 'customer'"
+        >
+          <div>By Email</div>
+          <div class="methos_image"><img src="../images/illustration_email@2x.png" /></div>
+        </el-checkbox>
+        <el-checkbox class="methodcheckbox" value="By System Message">
+          <div>By System Message</div>
+          <div class="methos_image">
+            <img :src="MethodsImg" />
+          </div>
+        </el-checkbox>
+      </el-checkbox-group>
+    </div>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.methodcheckbox {
+  align-items: start;
+  height: fit-content;
+  width: 49%;
+  margin-right: 5px;
+  background-color: var(--color-system-notification-bg);
+  border-radius: 6px;
+  padding: 11px 0 0 13px;
+}
+.methos_image {
+  margin: 9px 0;
+}
+</style>

+ 103 - 0
src/components/AddRules/src/components/RulesShipments.vue

@@ -0,0 +1,103 @@
+<script lang="ts" setup>
+import { ref, watch } from 'vue'
+
+interface OceanCheckboxItem {
+  value: string
+  label: string
+}
+interface Props {
+  CheckboxList: OceanCheckboxItem[]
+  Title: String
+  CheckedList: Array<''>
+}
+const props = defineProps<Props>()
+
+const OceanActive = ref(['OceanShipments'])
+const CheckedList = ref(props.CheckedList)
+const CheckboxList = ref(props.CheckboxList)
+watch(
+  () => props.CheckboxList,
+  (current) => {
+    CheckboxList.value = current
+  }
+)
+watch(
+  () => props.CheckedList,
+  (current) => {
+    CheckedList.value = current
+  }
+)
+
+const emit = defineEmits(['ChangeCheckRules'])
+const selectedLables = ref([])
+const CheckChange = (val: any) => {
+  selectedLables.value = []
+  // 遍历选中的value值,找到对应的label值
+  val.forEach((value) => {
+    const option = CheckboxList.value.find((item) => item.value === value)
+    if (option) {
+      selectedLables.value.push(option.label)
+    }
+  })
+  emit('ChangeCheckRules', selectedLables.value, CheckedList.value)
+}
+
+const hadleclose = (val:any) => {
+  selectedLables.value = []
+  // 遍历选中的value值,找到对应的code值
+  const option = CheckboxList.value.find((item) => item.label === val)
+  CheckedList.value = CheckedList.value.filter((item) => item !== option.value)
+}
+defineExpose({
+  hadleclose
+})
+</script>
+<template>
+  <div class="Ocean_collapse">
+    <el-collapse v-model="OceanActive">
+      <el-collapse-item name="OceanShipments">
+        <template #title>
+          <div class="Rules_Title OceanTitle">{{ props.Title }}</div>
+        </template>
+        <div class="oceanCheckbox">
+          <el-checkbox-group @change="CheckChange" v-model="CheckedList">
+            <el-checkbox
+              v-for="item in CheckboxList"
+              :key="item.label"
+              :label="item.label"
+              :value="item.value"
+            >
+              {{ item.label }}
+            </el-checkbox>
+          </el-checkbox-group>
+        </div>
+      </el-collapse-item>
+    </el-collapse>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+:deep(.el-checkbox-group) {
+  border: 1px solid var(--color-system-border);
+  border-radius: 5px;
+}
+:deep(.el-checkbox) {
+  width: 100%;
+  background-color: var(--color-system-body-bg);
+  border-bottom: 1px solid var(--color-system-border);
+  padding: 8px;
+}
+:deep(.el-checkbox:first-child) {
+  border-radius: 6px 6px 0 0;
+}
+:deep(.el-checkbox:last-child) {
+  border-radius: 0 0 6px 6px;
+  border-bottom: none;
+}
+.oceanCheckbox {
+  max-height: 321px;
+  overflow-x: hidden;
+  margin-bottom: 8px;
+  overflow-y: scroll;
+}
+</style>

+ 209 - 0
src/components/AddRules/src/components/ShipmentRange.vue

@@ -0,0 +1,209 @@
+<script lang="ts" setup>
+import { ref, computed } from 'vue'
+
+const OceanActive = ref(['TransportMode', 'Time'])
+const TransportCheckedList = ref([])
+interface OceanItem {
+  label: string
+  value: string
+}
+const TransportList = ref<OceanItem[]>([])
+TransportList.value = [
+  {
+    label: 'Ocean',
+    value: 'Ocean'
+  },
+  {
+    label: 'Air',
+    value: 'Air'
+  },
+  {
+    label: 'Road',
+    value: 'Road'
+  }
+]
+
+const TimeChecked = ref()
+
+const ETDTime = ref()
+const ETATime = ref()
+
+const clampedETDValue = computed({
+  get: () => ETDTime.value,
+  set: (newVal) => {
+    const isValid = /^[1-9]\d*$/.test(newVal);
+    ETDTime.value = isValid ? newVal : '';
+    if( ETDTime.value!='') {
+      ETDTime.value = Math.min(Math.max(parseInt(ETDTime.value, 10), 1), 365)
+    }
+  }
+})
+const clampedETAValue = computed({
+  get: () => ETATime.value,
+  set: (newVal) => {
+    const isValid = /^[1-9]\d*$/.test(newVal);
+    ETATime.value = isValid ? newVal : '';
+    if( ETATime.value!='') {
+      ETATime.value = Math.min(Math.max(parseInt(ETATime.value, 10), 1), 365)
+    }
+  }
+})
+
+let Transportstr: any = ''
+let Timestr: any = ''
+const emit = defineEmits(['ChangeCheckRules', 'ChangeCheckTimeRules'])
+const CheckChange = (val: any) => {
+  if (val != '') {
+    Transportstr = 'Transport Mode: ' + val
+  } else {
+    Transportstr = ''
+  }
+  emit('ChangeCheckRules', Transportstr)
+}
+
+// 输入ETD、ETA
+const changeTime = (val: any) => {
+  if (val == 1) {
+    if(clampedETDValue.value != '' && clampedETDValue.value != undefined) {
+      Timestr = 'ETD within ' + clampedETDValue.value + ' Day(s)'
+    } else{
+      Timestr = ''
+    }
+  } else if (val == 2) {
+    if(clampedETAValue.value != '' && clampedETAValue.value != undefined) {
+      Timestr = 'ETA within ' + clampedETAValue.value + ' Day(s)'
+    } else{
+      Timestr = ''
+    }
+  } else {
+    Timestr = ''
+  }
+  emit('ChangeCheckTimeRules', Timestr)
+}
+
+const handleCloseCreateRule = (val: any) => {
+  if (val.indexOf('ETD') != -1 || val.indexOf('ETA') != -1) {
+    TimeChecked.value = 0
+    ETDTime.value = ''
+    ETATime.value = ''
+  } else if (val.indexOf('Transport') != -1) {
+    TransportCheckedList.value = []
+  }
+}
+
+defineExpose({
+  handleCloseCreateRule
+})
+</script>
+<template>
+  <div class="Ocean_collapse">
+    <el-collapse v-model="OceanActive">
+      <el-collapse-item name="TransportMode">
+        <template #title>
+          <div class="Rules_Title OceanTitle">Transport Mode</div>
+        </template>
+        <div class="oceanCheckbox">
+          <el-checkbox-group @change="CheckChange" v-model="TransportCheckedList">
+            <el-checkbox
+              v-for="item in TransportList"
+              :key="item.label"
+              :label="item.label"
+              :value="item.value"
+            />
+          </el-checkbox-group>
+        </div>
+      </el-collapse-item>
+      <el-collapse-item name="Time">
+        <template #title>
+          <div class="Rules_Title OceanTitle">Time</div>
+        </template>
+        <div class="oceanCheckbox">
+          <el-radio-group v-model="TimeChecked" @change="changeTime">
+            <el-radio value="1">
+              <div class="flex">
+                ETD within
+                <el-input
+                  @input="changeTime('1')"
+                  v-model="clampedETDValue"
+                  class="input-with-select"
+                >
+                </el-input>
+                <div class="Days">Day(s)</div>
+              </div>
+            </el-radio>
+            <el-radio value="2">
+              <div class="flex">
+                ETA within
+                <el-input
+                  @input="changeTime('2')"
+                  v-model="clampedETAValue"
+                  class="input-with-select"
+                >
+                </el-input>
+                <div class="Days">Day(s)</div>
+              </div>
+            </el-radio>
+          </el-radio-group>
+        </div>
+      </el-collapse-item>
+    </el-collapse>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+:deep(.el-checkbox-group) {
+  border: 1px solid var(--color-select-border);
+  border-radius: 5px;
+}
+:deep(.el-checkbox) {
+  width: 100%;
+  background-color: var(--color-drawer-body-bg);
+  border-bottom: 1px solid var(--color-select-border);
+  padding: 8px;
+}
+:deep(.el-checkbox:first-child) {
+  border-radius: 6px 6px 0 0;
+}
+:deep(.el-checkbox:last-child) {
+  border-radius: 0 0 6px 6px;
+  border-bottom: none;
+}
+.oceanCheckbox {
+  max-height: 321px;
+  overflow-x: hidden;
+  margin-bottom: 8px;
+  overflow-y: scroll;
+}
+
+.input-with-select {
+  width: 96px;
+  border-radius: 6px 0 0 6px;
+}
+:deep(.el-input__wrapper) {
+  border-radius: 6px 0 0 6px;
+  opacity: 0.8;
+  height: 28px;
+}
+.flex {
+  display: flex;
+  align-items: center;
+}
+.Days {
+  width: 64px;
+  border: 1px solid var(--color-select-border);
+  border-radius: 0 6px 6px 0;
+  background-color: var(--color-drawer-body-bg);
+  border-left: 0;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  opacity: 0.8;
+  height: 28px;
+}
+:deep(.el-radio) {
+  background-color: var(--color-drawer-body-bg);
+}
+:deep( .el-radio__inner) {
+  border: 1px solid var(--color-system-checkbox-border);
+}
+</style>

二進制
src/components/AddRules/src/images/icon_collapse.png


二進制
src/components/AddRules/src/images/icon_expand.png


二進制
src/components/AddRules/src/images/icon_success_big@2x.png


二進制
src/components/AddRules/src/images/illustration_email@2x.png


二進制
src/components/AddRules/src/images/illustration_system massage@2x.png


二進制
src/components/AddRules/src/images/illustration_system massage_darkmode@2x.png


二進制
src/components/AddRules/src/images/submit_successful.png


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

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

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

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

+ 1465 - 0
src/components/CreateAddRules/src/CreateAddRules.vue

@@ -0,0 +1,1465 @@
+<script lang="ts" setup>
+import { ref, watch, onMounted } from 'vue'
+import RulesShipments from './components/RulesShipments.vue'
+import AddedrluesTag from './components/AddedrluesTag.vue'
+import DelayedType from './components/DelayedType.vue'
+import ETDShipments from './components/ETDShipments.vue'
+import NotiFrequency from './components/NotiFrequency.vue'
+import ShipmentRange from './components/ShipmentRange.vue'
+import NotiMethods from './components/NotiMethods.vue'
+import submitsucessful from './images/icon_success_big@2x.png'
+import { useRouter } from 'vue-router'
+import moment from 'moment-timezone'
+
+const router = useRouter()
+interface CheckboxItem {
+  value: string
+  label: string
+}
+
+interface Props {
+  TitleType: String
+}
+const MilestoneOceanListInit = ref<CheckboxItem[]>([])
+const MilestoneOceanListChecked = ref([])
+const MilestoneAirListInit = ref<CheckboxItem[]>([])
+const MilestoneAirListChecked = ref([])
+const ContainerOceanListInit = ref<CheckboxItem[]>([])
+const ContainerOceanListChecked = ref([])
+const props = defineProps<Props>()
+let savesubscribeobj: any = {}
+const RulesActive = ref([
+  'ShipmentRange',
+  'SelectMilestone',
+  'NotificationFrequency',
+  'NotificationMethod'
+])
+const OceanCheckList = ref([])
+const OceanCheckListCode = ref()
+const AirCheckList = ref([])
+const AirCheckListCode = ref()
+const ContainerOceanList = ref([])
+const ContainerOceanCode = ref()
+const IsFirstActive = ref(true)
+const IsTwoActive = ref(true)
+const IsThreeActive = ref(true)
+const IsFourActive = ref(true)
+const UnableSaveVisible = ref(false)
+const SaveVisibleError = ref(false)
+const SaveVisibleDetected = ref(false)
+const SaveedVisible = ref(false)
+const DelayedDeparturedList = ref([])
+const DelayedAirdList = ref([])
+const ETDOceanList = ref([])
+const ETDAirList = ref([])
+const FrequencyDataMil = ref()
+const FrequencyDataCon = ref()
+const FrequencyDataDep = ref()
+const FrequencyDataETD = ref()
+const MethodsDataMil = ref()
+const MethodsDataCon = ref()
+const MethodsDataDep = ref()
+const MethodsDataETD = ref()
+const DelayedDataInit = ref()
+const DelayedDataInitAir = ref()
+const OceanETDInit = ref()
+const AirETDInit = ref()
+const ShipmentRangeMil = ref()
+const ShipmentRangeCon = ref()
+const ShipmentRangeDep = ref()
+const ShipmentRangeETD = ref()
+
+const MonitoringList = ref()
+//选择create new rules
+const createListMilestone = ref([])
+const createListContainer = ref([])
+const createListDeparture = ref([])
+const createListETDChange = ref([])
+
+let createObj: any = {
+  Transportstr: '',
+  Timestr: ''
+}
+const getInitMonitoring = () => {
+  $api
+    .MonitoringInit({})
+    .then((res: any) => {
+      if (res.code === 200) {
+        MonitoringList.value = res.data
+        MilestoneOceanListInit.value = res.data.OceanCheckBoxList
+        MilestoneAirListInit.value = res.data.AirCheckBoxList
+        ContainerOceanListInit.value = res.data.CtnrCheckBoxList
+      }
+    })
+    .finally(() => {})
+}
+onMounted(() => {
+  getInitMonitoring()
+  Initdata()
+})
+// 初始赋值
+const editTableidtwo = ref('')
+const Initdata = () => {
+  if (sessionStorage.getItem('editTableid') != null) {
+    const editTableid = sessionStorage.getItem('editTableid')
+    editTableidtwo.value = sessionStorage.getItem('editTableid')
+    const editTablerules_type = sessionStorage.getItem('editTablerules_type')
+    $api
+      .EditMonitoringTable({
+        id: editTableid,
+        rules_type: editTablerules_type
+      })
+      .then((res: any) => {
+        if (res.code === 200) {
+          if (editTablerules_type == 'Milestone_Update') {
+            FrequencyDataMil.value = res.data.Milestone_Update
+            MethodsDataMil.value = res.data.Milestone_Update
+            ShipmentRangeMil.value = res.data.Milestone_Update
+            MilestoneOceanListInit.value = res.data.Milestone_Update.OceanCheckBoxList
+            OceanCheckListCode.value = []
+            MilestoneOceanListChecked.value = res.data.Milestone_Update.OceanCheckedList
+            OceanCheckListCode.value = res.data.Milestone_Update.OceanCheckedList
+            OceanCheckList.value = []
+            // 遍历选中的value值,找到对应的label值
+            MilestoneOceanListChecked.value.forEach((value) => {
+              const option = MilestoneOceanListInit.value.find((item) => item.value === value)
+              if (option) {
+                OceanCheckList.value.push(option.label)
+              }
+            })
+            AirCheckListCode.value = []
+            MilestoneAirListInit.value = res.data.Milestone_Update.AirCheckBoxList
+            MilestoneAirListChecked.value = res.data.Milestone_Update.AirCheckedList
+            AirCheckListCode.value = res.data.Milestone_Update.AirCheckedList
+            AirCheckList.value = []
+            // 遍历选中的value值,找到对应的label值
+            MilestoneAirListChecked.value.forEach((value) => {
+              const option = MilestoneAirListInit.value.find((item) => item.value === value)
+              if (option) {
+                AirCheckList.value.push(option.label)
+              }
+            })
+            createObj.Transportstr = res.data.Milestone_Update.shipment_details.split(';\r\n')[0]
+            createObj.Timestr = res.data.Milestone_Update.shipment_details.split(';\r\n')[1]
+            createListMilestone.value.push(createObj.Transportstr)
+            createListMilestone.value.push(createObj.Timestr)
+          } else if (editTablerules_type == 'Container_Status_Update') {
+            ContainerOceanCode.value = []
+            ContainerOceanListInit.value = res.data.Container_Status_Update.CtnrCheckBoxList
+            ContainerOceanListChecked.value = res.data.Container_Status_Update.CtnrCheckedList
+            ContainerOceanCode.value = res.data.Container_Status_Update.CtnrCheckedList
+            ContainerOceanList.value = []
+            // 遍历选中的value值,找到对应的label值
+            ContainerOceanListChecked.value.forEach((value) => {
+              const option = ContainerOceanListInit.value.find((item) => item.value === value)
+              if (option) {
+                ContainerOceanList.value.push(option.label)
+              }
+            })
+            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.Timestr = res.data.Container_Status_Update.shipment_details.split(';\r\n')[1]
+            createListContainer.value.push(createObj.Transportstr)
+            createListContainer.value.push(createObj.Timestr)
+          } else if (editTablerules_type == 'Departure/Arrival_Delay') {
+            let OceanObj: any = {}
+            OceanObj.atd_etd = res.data['Departure/Arrival_Delay'].ocean_atd_sub_etd
+            OceanObj.atd_etd_unit = res.data['Departure/Arrival_Delay'].ocean_atd_sub_etd_unit
+            OceanObj.ata_eta = res.data['Departure/Arrival_Delay'].ocean_ata_sub_eta
+            OceanObj.ata_eta_unit = res.data['Departure/Arrival_Delay'].ocean_ata_sub_eta_unit
+            DelayedDataInit.value = OceanObj
+            let AirObj: any = {}
+            AirObj.atd_etd = res.data['Departure/Arrival_Delay'].air_atd_sub_etd
+            AirObj.atd_etd_unit = res.data['Departure/Arrival_Delay'].air_atd_sub_etd_unit
+            AirObj.ata_eta = res.data['Departure/Arrival_Delay'].air_ata_sub_eta
+            AirObj.ata_eta_unit = res.data['Departure/Arrival_Delay'].air_ata_sub_eta_unit
+            DelayedDataInitAir.value = AirObj
+            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]
+            createListDeparture.value.push(createObj.Transportstr)
+            createListDeparture.value.push(createObj.Timestr)
+          } else if (editTablerules_type == 'ETD/ETA_Change') {
+            let OceanChange: any = {}
+            OceanChange.ETDradio = res.data['ETD/ETA_Change'].ocean_etd_change
+            OceanChange.etd_old_sub_new = res.data['ETD/ETA_Change'].ocean_etd_old_sub_new
+            OceanChange.etd_old_sub_new_unit = res.data['ETD/ETA_Change'].ocean_etd_old_sub_new_unit
+            OceanChange.ETAradio = res.data['ETD/ETA_Change'].ocean_eta_change
+            OceanChange.eta_old_sub_new = res.data['ETD/ETA_Change'].ocean_eta_old_sub_new
+            OceanChange.eta_old_sub_new_unit = res.data['ETD/ETA_Change'].ocean_eta_old_sub_new_unit
+            OceanETDInit.value = OceanChange
+            let AirChange: any = {}
+            AirChange.ETDradio = res.data['ETD/ETA_Change'].air_etd_change
+            AirChange.etd_old_sub_new = res.data['ETD/ETA_Change'].air_etd_old_sub_new
+            AirChange.etd_old_sub_new_unit = res.data['ETD/ETA_Change'].air_etd_old_sub_new_unit
+            AirChange.ETAradio = res.data['ETD/ETA_Change'].air_eta_change
+            AirChange.eta_old_sub_new = res.data['ETD/ETA_Change'].air_eta_old_sub_new
+            AirChange.eta_old_sub_new_unit = res.data['ETD/ETA_Change'].air_eta_old_sub_new_unit
+            AirETDInit.value = AirChange
+            FrequencyDataETD.value = res.data['ETD/ETA_Change']
+            MethodsDataETD.value = res.data['ETD/ETA_Change']
+            ShipmentRangeETD.value = res.data['ETD/ETA_Change']
+            createObj.Transportstr = res.data['ETD/ETA_Change'].shipment_details.split(';\r\n')[0]
+            createObj.Timestr = res.data['ETD/ETA_Change'].shipment_details.split(';\r\n')[1]
+            createListETDChange.value.push(createObj.Transportstr)
+            createListETDChange.value.push(createObj.Timestr)
+          }
+        }
+      })
+    setTimeout(() => {
+      sessionStorage.removeItem('editTableid')
+      sessionStorage.removeItem('editTablerules_type')
+    }, 1000)
+  }
+}
+
+// 给tag list赋值
+const ChangeCheckOceanRules = (val: any, value: any) => {
+  OceanCheckList.value = val
+  OceanCheckListCode.value = value
+}
+const ChangeContainerRules = (val: any, value: any) => {
+  ContainerOceanList.value = val
+  ContainerOceanCode.value = value
+}
+// delayed赋值
+const ChangeDeayedRules = (val: any) => {
+  DelayedDeparturedList.value = []
+  if (val.Departure != '') {
+    DelayedDeparturedList.value.push(val.Departure)
+    savesubscribeobj.ocean_atd_sub_etd = val.Departure.split(' ')[4]
+    savesubscribeobj.ocean_atd_sub_etd_unit = val.Departure.split(' ')[5]
+  } else {
+    delete savesubscribeobj.ocean_atd_sub_etd
+    delete savesubscribeobj.ocean_atd_sub_etd_unit
+  }
+  if (val.Arrival != '') {
+    DelayedDeparturedList.value.push(val.Arrival)
+    savesubscribeobj.ocean_ata_sub_eta = val.Arrival.split(' ')[4]
+    savesubscribeobj.ocean_ata_sub_eta_unit = val.Arrival.split(' ')[5]
+  } else {
+    delete savesubscribeobj.ocean_ata_sub_eta
+    delete savesubscribeobj.ocean_ata_sub_eta_unit
+  }
+}
+const ChangeAirRules = (val: any) => {
+  DelayedAirdList.value = []
+  if (val.Departure != '') {
+    DelayedAirdList.value.push(val.Departure)
+    savesubscribeobj.air_atd_sub_etd = val.Departure.split(' ')[4]
+    savesubscribeobj.air_atd_sub_etd_unit = val.Departure.split(' ')[5]
+  } else {
+    delete savesubscribeobj.air_atd_sub_etd
+    delete savesubscribeobj.air_atd_sub_etd_unit
+  }
+  if (val.Arrival != '') {
+    DelayedAirdList.value.push(val.Arrival)
+    savesubscribeobj.air_ata_sub_eta = val.Arrival.split(' ')[4]
+    savesubscribeobj.air_ata_sub_eta_unit = val.Arrival.split(' ')[5]
+  } else {
+    delete savesubscribeobj.air_ata_sub_eta
+    delete savesubscribeobj.air_ata_sub_eta_unit
+  }
+}
+const ChangeCheckAirRules = (val: any, value: any) => {
+  AirCheckList.value = val
+  AirCheckListCode.value = value
+}
+const changecheckCreateRulesMilestone = (val: any, value: any) => {
+  createListMilestone.value = []
+  createObj.Transportstr = val
+  if (createObj.Transportstr != '') {
+    createListMilestone.value.push(createObj.Transportstr)
+  }
+  if (createObj.Timestr != '') {
+    createListMilestone.value.push(createObj.Timestr)
+  }
+  savesubscribeobj.shipment_transport_mode = value
+  savesubscribeobj.shipment_details = createListMilestone.value.join(';\n')
+}
+const ChangeCheckTimeRulesMilestone = (val: any, time: any, timeend :any) => {
+  createListMilestone.value = []
+  createObj.Timestr = val
+  if (val.includes('ETD')) {
+    savesubscribeobj.shipment_etd_limit_from = time
+    savesubscribeobj.shipment_etd_limit = timeend
+    savesubscribeobj.shipment_eta_limit_from = ''
+    savesubscribeobj.shipment_eta_limit = ''
+  }
+  if (val.includes('ETA')) {
+    savesubscribeobj.shipment_eta_limit_from = time
+    savesubscribeobj.shipment_eta_limit = timeend
+    savesubscribeobj.shipment_etd_limit_from = ''
+    savesubscribeobj.shipment_etd_limit = ''
+  }
+  if (createObj.Transportstr != '') {
+    createListMilestone.value.push(createObj.Transportstr)
+  }
+  if (createObj.Timestr != '') {
+    createListMilestone.value.push(createObj.Timestr)
+  }
+  savesubscribeobj.shipment_details = createListMilestone.value.join(';\n')
+}
+const changecheckCreateRulesContainer = (val: any, value: any) => {
+  createListContainer.value = []
+  createObj.Transportstr = val
+  if (createObj.Transportstr != '') {
+    createListContainer.value.push(createObj.Transportstr)
+  }
+  if (createObj.Timestr != '') {
+    createListContainer.value.push(createObj.Timestr)
+  }
+  savesubscribeobj.shipment_transport_mode = value
+  savesubscribeobj.shipment_details = createListContainer.value.join(';\n')
+}
+const ChangeCheckTimeRulesContainer = (val: any, time: any, timeend :any) => {
+  createListContainer.value = []
+  createObj.Timestr = val
+  if (val.includes('ETD')) {
+    savesubscribeobj.shipment_etd_limit_from = time
+    savesubscribeobj.shipment_etd_limit = timeend
+    savesubscribeobj.shipment_eta_limit_from = ''
+    savesubscribeobj.shipment_eta_limit = ''
+  }
+  if (val.includes('ETA')) {
+    savesubscribeobj.shipment_eta_limit_from = time
+    savesubscribeobj.shipment_eta_limit = timeend
+    savesubscribeobj.shipment_etd_limit_from = ''
+    savesubscribeobj.shipment_etd_limit = ''
+  }
+  if (createObj.Transportstr != '') {
+    createListContainer.value.push(createObj.Transportstr)
+  }
+  if (createObj.Timestr != '') {
+    createListContainer.value.push(createObj.Timestr)
+  }
+  savesubscribeobj.shipment_details = createListContainer.value.join(';\n')
+}
+const changecheckCreateRulesDeparture = (val: any, value: any) => {
+  createListDeparture.value = []
+  createObj.Transportstr = val
+  if (createObj.Transportstr != '') {
+    createListDeparture.value.push(createObj.Transportstr)
+  }
+  if (createObj.Timestr != '') {
+    createListDeparture.value.push(createObj.Timestr)
+  }
+  savesubscribeobj.shipment_transport_mode = value
+  savesubscribeobj.shipment_details = createListDeparture.value.join(';\n')
+}
+const ChangeCheckTimeRulesDeparture = (val: any, time: any, timeend :any) => {
+  createListDeparture.value = []
+  createObj.Timestr = val
+  if (val.includes('ETD')) {
+    savesubscribeobj.shipment_etd_limit_from = time
+    savesubscribeobj.shipment_etd_limit = timeend
+    savesubscribeobj.shipment_eta_limit_from = ''
+    savesubscribeobj.shipment_eta_limit = ''
+  }
+  if (val.includes('ETA')) {
+    savesubscribeobj.shipment_eta_limit_from = time
+    savesubscribeobj.shipment_eta_limit = timeend
+    savesubscribeobj.shipment_etd_limit_from = ''
+    savesubscribeobj.shipment_etd_limit = ''
+  }
+  if (createObj.Transportstr != '') {
+    createListDeparture.value.push(createObj.Transportstr)
+  }
+  if (createObj.Timestr != '') {
+    createListDeparture.value.push(createObj.Timestr)
+  }
+  savesubscribeobj.shipment_details = createListDeparture.value.join(';\n')
+}
+const changecheckCreateRulesETDChange = (val: any, value: any) => {
+  createListETDChange.value = []
+  createObj.Transportstr = val
+  if (createObj.Transportstr != '') {
+    createListETDChange.value.push(createObj.Transportstr)
+  }
+  if (createObj.Timestr != '') {
+    createListETDChange.value.push(createObj.Timestr)
+  }
+  savesubscribeobj.shipment_transport_mode = value
+  savesubscribeobj.shipment_details = createListETDChange.value.join(';\n')
+}
+const ChangeCheckTimeRulesETDChange = (val: any, time: any, timeend :any) => {
+  createListETDChange.value = []
+  createObj.Timestr = val
+  if (val.includes('ETD')) {
+    savesubscribeobj.shipment_etd_limit_from = time
+    savesubscribeobj.shipment_etd_limit = timeend
+    savesubscribeobj.shipment_eta_limit_from = ''
+    savesubscribeobj.shipment_eta_limit = ''
+  }
+  if (val.includes('ETA')) {
+    savesubscribeobj.shipment_eta_limit_from = time
+    savesubscribeobj.shipment_eta_limit = timeend
+    savesubscribeobj.shipment_etd_limit_from = ''
+    savesubscribeobj.shipment_etd_limit = ''
+  }
+  if (createObj.Transportstr != '') {
+    createListETDChange.value.push(createObj.Transportstr)
+  }
+  if (createObj.Timestr != '') {
+    createListETDChange.value.push(createObj.Timestr)
+  }
+  savesubscribeobj.shipment_details = createListETDChange.value.join(';\n')
+}
+// 删除create tag
+const ShipmentRangeRef = ref()
+const handleCloseCreateRule = (val: any) => {
+  if (val.indexOf('ETD') != -1 || val.indexOf('ETA') != -1) {
+    createObj.Timestr = ''
+  } else if (val.indexOf('Transport') != -1) {
+    createObj.Transportstr = ''
+  }
+  ShipmentRangeRef.value.handleCloseCreateRule(val)
+}
+// 更改Frequency时间
+const MilFrequencyList = ref([])
+const ChangeMilFrequency = (val: any, type: any) => {
+  MilFrequencyList.value = val
+  savesubscribeobj = { ...savesubscribeobj, ...type }
+}
+const ConFrequencyList = ref([])
+const ChangeConFrequency = (val: any, type: any) => {
+  ConFrequencyList.value = val
+  savesubscribeobj = { ...savesubscribeobj, ...type }
+}
+const DepFrequencyList = ref([])
+const ChangeDepFrequency = (val: any, type: any) => {
+  DepFrequencyList.value = val
+  savesubscribeobj = { ...savesubscribeobj, ...type }
+}
+const ETDFrequencyList = ref([])
+const ChangeETDFrequency = (val: any, type: any) => {
+  ETDFrequencyList.value = val
+  savesubscribeobj = { ...savesubscribeobj, ...type }
+}
+
+// 删除 Frequency tag
+const NotiFrequencyDeleteMil = ref()
+const NotiFrequencyDeleteCon = ref()
+const NotiFrequencyDeleteDep = ref()
+const NotiFrequencyDeleteETD = ref()
+const handleCloseRadio = (val: any) => {
+  if (val == 'Mil') {
+    NotiFrequencyDeleteMil.value.handleCloseRadioFrequency()
+  } else if (val == 'Con') {
+    NotiFrequencyDeleteCon.value.handleCloseRadioFrequency()
+  } else if (val == 'Dep') {
+    NotiFrequencyDeleteDep.value.handleCloseRadioFrequency()
+  } else {
+    NotiFrequencyDeleteETD.value.handleCloseRadioFrequency()
+  }
+}
+
+// methods 切换
+const MilMethodsList = ref([])
+const ChangeMethodsAddMil = (val: any, type: any) => {
+  MilMethodsList.value = val
+  savesubscribeobj = { ...savesubscribeobj, ...type }
+}
+const ConMethodsList = ref([])
+const ChangeMethodsAddCon = (val: any, type: any) => {
+  ConMethodsList.value = val
+  savesubscribeobj = { ...savesubscribeobj, ...type }
+}
+const DepMethodsList = ref([])
+const ChangeMethodsAddDep = (val: any, type: any) => {
+  DepMethodsList.value = val
+  savesubscribeobj = { ...savesubscribeobj, ...type }
+}
+const ETDMethodsList = ref([])
+const ChangeMethodsAddETD = (val: any, type: any) => {
+  ETDMethodsList.value = val
+  savesubscribeobj = { ...savesubscribeobj, ...type }
+}
+const OceanDelayed = ref()
+const AirDelayed = ref()
+
+const handleCloseDelayed = (val: any) => {
+  OceanDelayed.value.closeDelayed(val)
+}
+const handleCloseAirDelayed = (val: any) => {
+  AirDelayed.value.closeDelayed(val)
+}
+// ETD Change
+const ChangeETDOceanRules = (val: any) => {
+  ETDOceanList.value = []
+  if (val.ETD != '') {
+    if (val.ETD.indexOf('≥') != -1) {
+      savesubscribeobj.ocean_etd_change = false
+      savesubscribeobj.ocean_etd_old_sub_new = val.ETD.split(' ')[7]
+      savesubscribeobj.ocean_etd_old_sub_new_unit = val.ETD.split(' ')[8]
+    } else {
+      savesubscribeobj.ocean_etd_change = true
+    }
+    ETDOceanList.value.push(val.ETD)
+  } else {
+    delete savesubscribeobj.ocean_etd_change
+    delete savesubscribeobj.ocean_etd_old_sub_new
+    delete savesubscribeobj.ocean_etd_old_sub_new_unit
+  }
+  if (val.ETA != '') {
+    if (val.ETA.indexOf('≥') != -1) {
+      savesubscribeobj.ocean_eta_change = false
+      savesubscribeobj.ocean_eta_old_sub_new = val.ETA.split(' ')[7]
+      savesubscribeobj.ocean_eta_old_sub_new_unit = val.ETA.split(' ')[8]
+    } else {
+      savesubscribeobj.ocean_eta_change = true
+    }
+    ETDOceanList.value.push(val.ETA)
+  } else {
+    delete savesubscribeobj.ocean_eta_change
+    delete savesubscribeobj.ocean_eta_old_sub_new
+    delete savesubscribeobj.ocean_eta_old_sub_new_unit
+  }
+}
+// 删除ETD tag
+const OceanETD = ref()
+const closeOceanETD = (val: any) => {
+  OceanETD.value.closeETD(val)
+}
+// ETA Change
+const ChangeETDAirRules = (val: any) => {
+  ETDAirList.value = []
+  if (val.ETD != '') {
+    if (val.ETD.indexOf('≥') != -1) {
+      savesubscribeobj.air_etd_change = false
+      savesubscribeobj.air_etd_old_sub_new = val.ETD.split(' ')[7]
+      savesubscribeobj.air_etd_old_sub_new_unit = val.ETD.split(' ')[8]
+    } else {
+      savesubscribeobj.air_etd_change = true
+    }
+    ETDAirList.value.push(val.ETD)
+  } else {
+    delete savesubscribeobj.air_etd_change
+    delete savesubscribeobj.air_etd_old_sub_new
+    delete savesubscribeobj.air_etd_old_sub_new_unit
+  }
+  if (val.ETA != '') {
+    if (val.ETA.indexOf('≥') != -1) {
+      savesubscribeobj.air_eta_change = false
+      savesubscribeobj.air_eta_old_sub_new = val.ETA.split(' ')[7]
+      savesubscribeobj.air_eta_old_sub_new_unit = val.ETA.split(' ')[8]
+    } else {
+      savesubscribeobj.air_eta_change = true
+    }
+    ETDAirList.value.push(val.ETA)
+  } else {
+    delete savesubscribeobj.air_eta_change
+    delete savesubscribeobj.air_eta_old_sub_new
+    delete savesubscribeobj.air_eta_old_sub_new_unit
+  }
+}
+
+// 删除ETA tag
+const AirETD = ref()
+const closeAirETD = (val: any) => {
+  AirETD.value.closeETD(val)
+}
+
+// 保存subscribe配置
+const missingmessage = ref('')
+let defaultTimeZone = 'UTC' + moment().tz(moment.tz.guess()).format('Z')
+// 保存成功调用接口
+const SaveSuceessful = () => {
+  $api
+    .MonitoringSave({
+      ...savesubscribeobj,
+      is_similar_rule: false,
+      id: editTableidtwo.value,
+      default_time_zone: defaultTimeZone
+    })
+    .then((res: any) => {
+      if (res.code === 200) {
+        if(res.data.msg == 'Update Successful') {
+          SaveedVisible.value = true
+          setTimeout(() => {
+            SaveedVisible.value = false
+            sessionStorage.setItem('activeTab', 'Monitoring Settings')
+            router.push({
+              path: '/SystemSettings',
+              query: {}
+            })
+          }, 3000)
+        } else if(res.data.msg == 'Similar Rule Detected') {
+          SaveVisibleDetected.value = true
+        } else if(res.data.msg == 'Unable to Save')
+        SaveVisibleError.value = true
+      }
+    })
+}
+
+const HandelSaveVisibleDetected = () => {
+  SaveVisibleDetected.value = false
+  $api
+    .MonitoringSave({
+      ...savesubscribeobj,
+      is_similar_rule: true,
+      id: editTableidtwo.value
+    })
+    .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)
+      }
+    })
+}
+const Savesubscribe = () => {
+  missingmessage.value = ''
+  let str = ''
+  if (props.TitleType == 'Milestone') {
+    savesubscribeobj.rules_type = 'Milestone_Update'
+    if (
+      OceanCheckList.value.length == 0 &&
+      AirCheckList.value.length == 0 ||
+      MilFrequencyList.value.length == 0 ||
+      MilMethodsList.value.length == 0 ||
+      createObj.Transportstr == '' ||
+      createObj.Timestr == ''
+    ) {
+      if (createObj.Transportstr == '') {
+        missingmessage.value += 'Transport Mode, '
+      }
+      if (createObj.Timestr == '') {
+        missingmessage.value += 'Time, '
+      }
+      if (OceanCheckList.value.length == 0 && AirCheckList.value.length == 0 ) {
+        missingmessage.value += 'Select Milestone, '
+      }
+      if (MilFrequencyList.value.length == 0) {
+        missingmessage.value += 'Notification Frequency, '
+      }
+      if (MilMethodsList.value.length == 0) {
+        missingmessage.value += 'Notification Method, '
+      }
+      missingmessage.value = missingmessage.value.substring(0, missingmessage.value.length - 2)
+      UnableSaveVisible.value = true
+    } 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(',')
+      } else {
+        str =
+        'Ocean Milestones: ' +
+        OceanCheckList.value.join(',') +
+        ';\nAir Milestones: ' +
+        AirCheckList.value.join(',') +
+        ';'
+      }
+      savesubscribeobj.event_details = str
+      SaveSuceessful()
+    }
+  } else if (props.TitleType == 'Container') {
+    savesubscribeobj.rules_type = 'Container_Status_Update'
+    if (
+      ContainerOceanList.value.length == 0 ||
+      ConFrequencyList.value.length == 0 ||
+      ConMethodsList.value.length == 0 ||
+      createObj.Transportstr == '' ||
+      createObj.Timestr == ''
+    ) {
+      if (createObj.Transportstr == '') {
+        missingmessage.value += 'Transport Mode, '
+      }
+      if (createObj.Timestr == '') {
+        missingmessage.value += 'Time, '
+      }
+      if (ContainerOceanList.value.length == 0) {
+        missingmessage.value += 'Container Status, '
+      }
+      if (ConFrequencyList.value.length == 0) {
+        missingmessage.value += 'Notification Frequency, '
+      }
+      if (ConMethodsList.value.length == 0) {
+        missingmessage.value += 'Notification Method, '
+      }
+      missingmessage.value = missingmessage.value.substring(0, missingmessage.value.length - 2)
+      UnableSaveVisible.value = true
+    } else {
+      savesubscribeobj.ocean_ctnr_status = ContainerOceanCode.value
+      str = 'Ocean Container: ' + ContainerOceanList.value.join(',')
+      savesubscribeobj.event_details = str
+      SaveSuceessful()
+    }
+  } else if (props.TitleType == 'Departure') {
+    savesubscribeobj.rules_type = 'Departure/Arrival_Delay'
+    if ( 
+      DelayedDeparturedList.value.length == 0 &&
+      DelayedAirdList.value.length == 0 ||
+      DepFrequencyList.value.length == 0 ||
+      DepMethodsList.value == undefined ||
+      DepMethodsList.value.length == 0 ||
+      createObj.Transportstr == '' ||
+      createObj.Timestr == ''
+    ) {
+      if (createObj.Transportstr == '') {
+        missingmessage.value += 'Transport Mode, '
+      }
+      if (createObj.Timestr == '') {
+        missingmessage.value += 'Time, '
+      }
+      if (DelayedDeparturedList.value.length == 0 && DelayedAirdList.value.length == 0) {
+        missingmessage.value += 'Select Delayed Shipments, '
+      }
+      if (DepFrequencyList.value.length == 0) {
+        missingmessage.value += 'Notification Frequency, '
+      }
+      if (DepMethodsList.value.length == 0) {
+        missingmessage.value += 'Notification Method, '
+      }
+      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(',')
+      } else {
+        str =
+        'Ocean: ' +
+        DelayedDeparturedList.value.join(',') +
+        ';\nAir: ' +
+        DelayedAirdList.value.join(',') +
+        ';'
+      }
+      savesubscribeobj.event_details = str
+      SaveSuceessful()
+    }
+  } else {
+    savesubscribeobj.rules_type = 'ETD/ETA_Change'
+    if (
+      ETDOceanList.value.length == 0 &&
+      ETDAirList.value.length == 0 ||
+      ETDFrequencyList.value.length == 0 ||
+      ETDMethodsList.value == undefined ||
+      ETDMethodsList.value.length == 0 ||
+      createObj.Transportstr == '' ||
+      createObj.Timestr == ''
+    ) {
+      if (createObj.Transportstr == '') {
+        missingmessage.value += 'Transport Mode, '
+      }
+      if (createObj.Timestr == '') {
+        missingmessage.value += 'Time, '
+      }
+      if (ETDOceanList.value.length == 0 && ETDAirList.value.length == 0) {
+        missingmessage.value += 'Select Time Type, '
+      }
+      if (ETDFrequencyList.value.length == 0) {
+        missingmessage.value += 'Notification Frequency, '
+      }
+      if (ETDMethodsList.value.length == 0) {
+        missingmessage.value += 'Notification Method, '
+      }
+      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(',')
+      } else {
+        str =
+        '[Ocean]' +
+        ETDOceanList.value.join(',') +
+        ';\n[Air]' +
+        ETDAirList.value.join(',') +
+        ';'
+      }
+      savesubscribeobj.event_details = str
+      SaveSuceessful()
+    }
+  }
+}
+
+// 删除表格数据后清空所有数据
+const clearData = (val: any) => {
+  if (val == 'Milestone') {
+    OceanCheckList.value = []
+    AirCheckList.value = []
+    MilFrequencyList.value = []
+    MilMethodsList.value = []
+    MilestoneOceanListChecked.value = []
+    MilestoneAirListChecked.value = []
+    createListMilestone.value = []
+  } else if (val == 'Container') {
+    ContainerOceanList.value = []
+    createListContainer.value = []
+    ConFrequencyList.value = []
+    ConMethodsList.value = []
+    ContainerOceanListChecked.value = []
+  } else if (val == 'Departure') {
+    DelayedDeparturedList.value = []
+    DelayedAirdList.value = []
+    DepFrequencyList.value = []
+    DepMethodsList.value = []
+    createListDeparture.value = []
+  } else {
+    ETDOceanList.value = []
+    ETDAirList.value = []
+    ETDFrequencyList.value = []
+    ETDMethodsList.value = []
+    createListETDChange.value = []
+  }
+}
+
+// 删除Oceam shipments
+const MilOceanref = ref()
+const MilAirref = ref()
+const ContainerOcean = ref()
+const handleCloseMilestoneOcean = (val:any) => {
+  MilOceanref.value.hadleclose(val)
+}
+const handleCloseMilestoneAir = (val:any) => {
+  MilAirref.value.hadleclose(val)
+}
+
+const handleCloseContainer = (val:any) => {
+  ContainerOcean.value.hadleclose(val)
+}
+
+defineExpose({
+  clearData,
+  Savesubscribe,
+  Initdata
+})
+</script>
+<template>
+  <div class="Rules_flex">
+    <div class="Rules_left">
+      <div class="Rules_collapse">
+        <el-collapse v-model="RulesActive" @change="IsFourActive = !IsFourActive">
+          <el-collapse-item name="ShipmentRange">
+            <template #title>
+              <div class="Rules_Title">
+                <span class="iconfont_icon icon_dark">
+                  <svg class="iconfont" aria-hidden="true">
+                    <use
+                      :xlink:href="IsFourActive ? '#icon-icon_dropdown_b' : '#icon-icon_up_b'"
+                    ></use>
+                  </svg>
+                </span>
+                <span class="stars_red">*</span>Shipment Range
+              </div>
+            </template>
+            <div>
+              <ShipmentRange
+                ref="ShipmentRangeRef"
+                v-if="props.TitleType == 'Milestone'"
+                :ShipmentRangeData="ShipmentRangeMil"
+                @ChangeCheckRules="changecheckCreateRulesMilestone"
+                @ChangeCheckTimeRules="ChangeCheckTimeRulesMilestone"
+              ></ShipmentRange>
+              <ShipmentRange
+                ref="ShipmentRangeRef"
+                v-if="props.TitleType == 'Container'"
+                :ShipmentRangeData="ShipmentRangeCon"
+                @ChangeCheckRules="changecheckCreateRulesContainer"
+                @ChangeCheckTimeRules="ChangeCheckTimeRulesContainer"
+              ></ShipmentRange>
+              <ShipmentRange
+                ref="ShipmentRangeRef"
+                v-if="props.TitleType == 'Departure'"
+                :ShipmentRangeData="ShipmentRangeDep"
+                @ChangeCheckRules="changecheckCreateRulesDeparture"
+                @ChangeCheckTimeRules="ChangeCheckTimeRulesDeparture"
+              ></ShipmentRange>
+              <ShipmentRange
+                ref="ShipmentRangeRef"
+                v-if="props.TitleType == 'ETDChange'"
+                :ShipmentRangeData="ShipmentRangeETD"
+                @ChangeCheckRules="changecheckCreateRulesETDChange"
+                @ChangeCheckTimeRules="ChangeCheckTimeRulesETDChange"
+              ></ShipmentRange>
+            </div>
+          </el-collapse-item>
+        </el-collapse>
+        <el-collapse
+          v-model="RulesActive"
+          @change="IsFirstActive = !IsFirstActive"
+          v-if="props.TitleType == 'Milestone'"
+        >
+          <el-collapse-item name="SelectMilestone">
+            <template #title>
+              <div class="Rules_Title">
+                <span class="iconfont_icon icon_dark">
+                  <svg class="iconfont" aria-hidden="true">
+                    <use
+                      :xlink:href="IsFirstActive ? '#icon-icon_dropdown_b' : '#icon-icon_up_b'"
+                    ></use>
+                  </svg>
+                </span>
+                <span class="stars_red">*</span>Select Milestone
+              </div>
+            </template>
+            <div>
+              <RulesShipments
+                Title="Ocean Shipments"
+                ref="MilOceanref"
+                @ChangeCheckRules="ChangeCheckOceanRules"
+                :CheckboxList="MilestoneOceanListInit"
+                :CheckedList="MilestoneOceanListChecked"
+              ></RulesShipments>
+            </div>
+            <div>
+              <RulesShipments
+                Title="Air Shipments"
+                ref="MilAirref"
+                @ChangeCheckRules="ChangeCheckAirRules"
+                :CheckboxList="MilestoneAirListInit"
+                :CheckedList="MilestoneAirListChecked"
+              ></RulesShipments>
+            </div>
+          </el-collapse-item>
+        </el-collapse>
+        <el-collapse
+          v-model="RulesActive"
+          @change="IsFirstActive = !IsFirstActive"
+          v-if="props.TitleType == 'Container'"
+        >
+          <el-collapse-item name="SelectMilestone">
+            <template #title>
+              <div class="Rules_Title">
+                <span class="iconfont_icon icon_dark">
+                  <svg class="iconfont" aria-hidden="true">
+                    <use
+                      :xlink:href="IsFirstActive ? '#icon-icon_dropdown_b' : '#icon-icon_up_b'"
+                    ></use>
+                  </svg>
+                </span>
+                <span class="stars_red">*</span>Select Container Status
+              </div>
+            </template>
+            <div>
+              <RulesShipments
+                Title="Ocean Shipments"
+                ref="ContainerOcean"
+                @ChangeCheckRules="ChangeContainerRules"
+                :CheckboxList="ContainerOceanListInit"
+                :CheckedList="ContainerOceanListChecked"
+              ></RulesShipments>
+            </div>
+          </el-collapse-item>
+        </el-collapse>
+        <el-collapse
+          v-model="RulesActive"
+          @change="IsFirstActive = !IsFirstActive"
+          v-if="props.TitleType == 'Departure'"
+        >
+          <el-collapse-item name="SelectMilestone">
+            <template #title>
+              <div class="Rules_Title">
+                <span class="iconfont_icon icon_dark">
+                  <svg class="iconfont" aria-hidden="true">
+                    <use
+                      :xlink:href="IsFirstActive ? '#icon-icon_dropdown_b' : '#icon-icon_up_b'"
+                    ></use>
+                  </svg>
+                </span>
+                <span class="stars_red">*</span>Select Delayed Type
+              </div>
+            </template>
+            <div>
+              <DelayedType
+                Title="Ocean Shipments"
+                ref="OceanDelayed"
+                :DelayedData="DelayedDataInit"
+                @ChangeCheckRules="ChangeDeayedRules"
+              ></DelayedType>
+            </div>
+            <div>
+              <DelayedType
+                Title="Air Shipments"
+                ref="AirDelayed"
+                :DelayedData="DelayedDataInitAir"
+                @ChangeCheckRules="ChangeAirRules"
+              ></DelayedType>
+            </div>
+          </el-collapse-item>
+        </el-collapse>
+        <el-collapse
+          v-model="RulesActive"
+          @change="IsFirstActive = !IsFirstActive"
+          v-if="props.TitleType == 'ETDChange'"
+        >
+          <el-collapse-item name="SelectMilestone">
+            <template #title>
+              <div class="Rules_Title">
+                <span class="iconfont_icon icon_dark">
+                  <svg class="iconfont" aria-hidden="true">
+                    <use
+                      :xlink:href="IsFirstActive ? '#icon-icon_dropdown_b' : '#icon-icon_up_b'"
+                    ></use>
+                  </svg>
+                </span>
+                <span class="stars_red">*</span>Select Time Type
+              </div>
+            </template>
+            <div>
+              <ETDShipments
+                Title="Ocean Shipments"
+                ref="OceanETD"
+                :ETDData="OceanETDInit"
+                @ChangeCheckRules="ChangeETDOceanRules"
+              ></ETDShipments>
+            </div>
+            <div>
+              <ETDShipments
+                Title="Air Shipments"
+                ref="AirETD"
+                :ETDData="AirETDInit"
+                @ChangeCheckRules="ChangeETDAirRules"
+              ></ETDShipments>
+            </div>
+          </el-collapse-item>
+        </el-collapse>
+        <el-collapse
+          style="margin-top: 17px; margin-right: 16px"
+          v-model="RulesActive"
+          @change="IsTwoActive = !IsTwoActive"
+        >
+          <el-collapse-item name="NotificationFrequency">
+            <template #title>
+              <div class="Rules_Title">
+                <span class="iconfont_icon icon_dark">
+                  <svg class="iconfont" aria-hidden="true">
+                    <use
+                      :xlink:href="IsTwoActive ? '#icon-icon_dropdown_b' : '#icon-icon_up_b'"
+                    ></use>
+                  </svg>
+                </span>
+                <span class="stars_red">*</span>Notification Frequency
+              </div>
+            </template>
+            <NotiFrequency
+              v-if="props.TitleType == 'Milestone'"
+              ref="NotiFrequencyDeleteMil"
+              :FrequencyData="FrequencyDataMil"
+              @ChangeFrequencyAdd="ChangeMilFrequency"
+            ></NotiFrequency>
+            <NotiFrequency
+              v-if="props.TitleType == 'Container'"
+              ref="NotiFrequencyDeleteCon"
+              :FrequencyData="FrequencyDataCon"
+              @ChangeFrequencyAdd="ChangeConFrequency"
+            ></NotiFrequency>
+            <NotiFrequency
+              v-if="props.TitleType == 'Departure'"
+              ref="NotiFrequencyDeleteDep"
+              :FrequencyData="FrequencyDataDep"
+              @ChangeFrequencyAdd="ChangeDepFrequency"
+            ></NotiFrequency>
+            <NotiFrequency
+              v-if="props.TitleType == 'ETDChange'"
+              ref="NotiFrequencyDeleteETD"
+              :FrequencyData="FrequencyDataETD"
+              @ChangeFrequencyAdd="ChangeETDFrequency"
+            ></NotiFrequency>
+          </el-collapse-item>
+        </el-collapse>
+        <el-collapse
+          style="margin: 17px 0"
+          v-model="RulesActive"
+          @change="IsThreeActive = !IsThreeActive"
+        >
+          <el-collapse-item name="NotificationMethod">
+            <template #title>
+              <div class="Rules_Title">
+                <span class="iconfont_icon icon_dark">
+                  <svg class="iconfont" aria-hidden="true">
+                    <use
+                      :xlink:href="IsThreeActive ? '#icon-icon_dropdown_b' : '#icon-icon_up_b'"
+                    ></use>
+                  </svg>
+                </span>
+                <span class="stars_red">*</span>Notification Method
+              </div>
+            </template>
+            <NotiMethods
+              v-if="props.TitleType == 'Milestone'"
+              :MethodsData="MethodsDataMil"
+              @ChangeMethodsAdd="ChangeMethodsAddMil"
+            ></NotiMethods>
+            <NotiMethods
+              v-if="props.TitleType == 'Container'"
+              :MethodsData="MethodsDataCon"
+              @ChangeMethodsAdd="ChangeMethodsAddCon"
+            ></NotiMethods>
+            <NotiMethods
+              v-if="props.TitleType == 'Departure'"
+              :MethodsData="MethodsDataDep"
+              @ChangeMethodsAdd="ChangeMethodsAddDep"
+            ></NotiMethods>
+            <NotiMethods
+              v-if="props.TitleType == 'ETDChange'"
+              :MethodsData="MethodsDataETD"
+              @ChangeMethodsAdd="ChangeMethodsAddETD"
+            ></NotiMethods>
+          </el-collapse-item>
+        </el-collapse>
+      </div>
+    </div>
+    <div class="Rules_right">
+      <div class="right_Title">Added Rules</div>
+      <AddedrluesTag
+        v-if="props.TitleType == 'Milestone'"
+        :CheckedList="createListMilestone"
+        Title="Shipment Range"
+        @handleCloseRadio="handleCloseCreateRule"
+      ></AddedrluesTag>
+      <AddedrluesTag
+        v-if="props.TitleType == 'Container'"
+        :CheckedList="createListContainer"
+        Title="Shipment Range"
+        @handleCloseRadio="handleCloseCreateRule"
+      ></AddedrluesTag>
+      <AddedrluesTag
+        v-if="props.TitleType == 'Departure'"
+        :CheckedList="createListDeparture"
+        Title="Shipment Range"
+        @handleCloseRadio="handleCloseCreateRule"
+      ></AddedrluesTag>
+      <AddedrluesTag
+        v-if="props.TitleType == 'ETDChange'"
+        :CheckedList="createListETDChange"
+        Title="Shipment Range"
+        @handleCloseRadio="handleCloseCreateRule"
+      ></AddedrluesTag>
+      <AddedrluesTag
+        :CheckedList="DelayedDeparturedList"
+        v-if="props.TitleType == 'Departure'"
+        @handleCloseRadio="handleCloseDelayed"
+        Title="Ocean Shipments"
+      ></AddedrluesTag>
+      <AddedrluesTag
+        v-if="props.TitleType == 'Milestone'"
+        @handleCloseRadio="handleCloseMilestoneOcean"
+        :CheckedList="OceanCheckList"
+        Title="Ocean Shipments"
+      ></AddedrluesTag>
+      <AddedrluesTag
+        v-if="props.TitleType == 'Container'"
+        :CheckedList="ContainerOceanList"
+        @handleCloseRadio="handleCloseContainer"
+        Title="Ocean Shipments"
+      ></AddedrluesTag>
+      <AddedrluesTag
+        :CheckedList="ETDOceanList"
+        v-if="props.TitleType == 'ETDChange'"
+        Title="Ocean Shipments"
+        @handleCloseRadio="closeOceanETD"
+      ></AddedrluesTag>
+      <AddedrluesTag
+        :CheckedList="ETDAirList"
+        v-if="props.TitleType == 'ETDChange'"
+        @handleCloseRadio="closeAirETD"
+        Title="Air Shipments"
+      ></AddedrluesTag>
+      <AddedrluesTag
+        :CheckedList="DelayedAirdList"
+        v-if="props.TitleType == 'Departure'"
+        @handleCloseRadio="handleCloseAirDelayed"
+        Title="Air Shipments"
+      ></AddedrluesTag>
+      <AddedrluesTag
+        v-if="props.TitleType == 'Milestone'"
+        :CheckedList="AirCheckList"
+        @handleCloseRadio="handleCloseMilestoneAir"
+        Title="Air Shipments"
+      ></AddedrluesTag>
+      <AddedrluesTag
+        v-if="props.TitleType == 'Milestone'"
+        :CheckedList="MilFrequencyList"
+        Title="Notification Frequency"
+        @handleCloseRadio="handleCloseRadio('Mil')"
+      ></AddedrluesTag>
+      <AddedrluesTag
+        v-if="props.TitleType == 'Container'"
+        :CheckedList="ConFrequencyList"
+        Title="Notification Frequency"
+        @handleCloseRadio="handleCloseRadio('Con')"
+      ></AddedrluesTag>
+      <AddedrluesTag
+        v-if="props.TitleType == 'Departure'"
+        :CheckedList="DepFrequencyList"
+        Title="Notification Frequency"
+        @handleCloseRadio="handleCloseRadio('Dep')"
+      ></AddedrluesTag>
+      <AddedrluesTag
+        v-if="props.TitleType == 'ETDChange'"
+        :CheckedList="ETDFrequencyList"
+        Title="Notification Frequency"
+        @handleCloseRadio="handleCloseRadio('ETD')"
+      ></AddedrluesTag>
+      <AddedrluesTag
+        v-if="props.TitleType == 'Milestone'"
+        :CheckedList="MilMethodsList"
+        Title="Notification Method"
+      ></AddedrluesTag>
+      <AddedrluesTag
+        v-if="props.TitleType == 'Container'"
+        :CheckedList="ConMethodsList"
+        Title="Notification Method"
+      ></AddedrluesTag>
+      <AddedrluesTag
+        v-if="props.TitleType == 'Departure'"
+        :CheckedList="DepMethodsList"
+        Title="Notification Method"
+      ></AddedrluesTag>
+      <AddedrluesTag
+        v-if="props.TitleType == 'ETDChange'"
+        :CheckedList="ETDMethodsList"
+        Title="Notification Method"
+      ></AddedrluesTag>
+    </div>
+  </div>
+  <el-dialog v-model="UnableSaveVisible" width="480">
+    <div>{{ missingmessage }} missing.</div>
+    <div>Please complete all required fields.</div>
+    <template #footer>
+      <div class="dialog-footer">
+        <el-button
+          class="el-button--danger"
+          @click="UnableSaveVisible = 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="SaveedVisible" width="320" style="height: 212px">
+    <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>
+    <!-- 三项重合提示 -->
+  <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>
+</template>
+
+<style lang="scss" scoped>
+.Rules_flex {
+  padding: 0 0 0 8px;
+  display: flex;
+}
+.Rules_left {
+  width: 60%;
+  border-right: 1px solid var(--color-user-config-title-bottom-border);
+}
+.Rules_right {
+  width: 40%;
+  background-color: var(--add-rules-background-color);
+}
+.right_Title {
+  color: var(--color-neutral-1);
+  padding: 9px 8px;
+  font-size: 18px;
+  font-weight: 700;
+  border-bottom: 1px solid var(--color-user-config-title-bottom-border);
+  background-color: var(--color-drawer-body-bg);
+}
+.iconfont {
+  width: 16px;
+  height: 16px;
+  margin-right: 8px;
+}
+.icon_danger {
+  fill: var(--color-btn-danger-bg);
+}
+:deep(.Rules_Title) {
+  color: var(--color-neutral-1);
+  font-size: 14px;
+  font-weight: 700;
+  height: 22px;
+  display: flex;
+  align-items: center;
+}
+.stars_red {
+  color: var(--color-danger);
+}
+:deep(.el-collapse-item__header) {
+  height: 25px !important;
+  margin: 14px 0 0 0 !important;
+  border: none !important;
+  padding: 0 !important;
+}
+.Ocean_collapse {
+  :deep(.el-collapse-item__header) {
+    margin: 4px 0 0 0 !important;
+  }
+}
+:deep(.el-collapse-item__header):hover {
+  background-color: var(--color-system-color-bg) !important;
+  border: none !important;
+}
+:deep(.el-collapse-item__arrow) {
+  width: 0 !important;
+  height: 0 !important;
+}
+:deep(.el-icon svg) {
+  width: 0 !important;
+}
+:deep(.el-collapse-item__header.is-active) {
+  background-color: transparent !important;
+  border-color: transparent !important;
+}
+:deep(.el-collapse-item__arrow.is-active) {
+  transform: rotate(-180deg) !important;
+}
+:deep(.Ocean_collapse .el-collapse-item__arrow) {
+  width: 16px !important;
+  height: 16px !important;
+  background-image: url('../src/images/icon_expand.png') !important;
+  background-size: contain;
+  background-repeat: no-repeat;
+  transform: rotate(0);
+}
+:deep(.Ocean_collapse .el-collapse-item__header) {
+  background-color: var(--color-system-color-bg) !important;
+  padding: 0 8px !important;
+  height: 40px !important;
+}
+:deep(.Ocean_collapse .el-collapse-item) {
+  background-color: var(--color-system-color-bg);
+  border-radius: 12px;
+}
+:deep(.Ocean_collapse .el-collapse-item__wrap) {
+  padding: 0 8px !important;
+}
+:deep(.Ocean_collapse .el-collapse-item__header.is-active) {
+  background-color: var(--color-system-color-bg) !important;
+}
+:deep(.el-checkbox__input.is-checked + .el-checkbox__label) {
+  color: var(--color-neutral-1);
+}
+.Rules_buttom {
+  padding: 8px;
+  border-top: 1px solid var(--color-system-border-1);
+}
+.rules_button {
+  width: 100px;
+  height: 40px;
+}
+.cancel_header {
+  font-size: 18px;
+  font-weight: 700;
+  color: var(--color-neutral-1);
+  display: flex;
+  align-items: center;
+}
+.icon_warning {
+  width: 22px;
+  height: 22px;
+  margin-right: 0;
+  fill: var(--color-btn-warning-bg);
+}
+.iconfont_warning {
+  display: flex;
+  align-items: center;
+}
+:deep(header.el-dialog__header) {
+  background-color: var(--color-system-body-bg);
+}
+:deep(footer.el-dialog__footer) {
+  border-top: none;
+}
+:deep(.el-collapse) {
+  margin-right: 8px;
+}
+</style>

+ 79 - 0
src/components/CreateAddRules/src/components/AddedrluesTag.vue

@@ -0,0 +1,79 @@
+<script setup lang="ts" >
+import { ref, watch } from 'vue'
+const props = defineProps({
+  CheckedList: Array,
+  Title: String
+})
+const CheckedList = ref(props.CheckedList)
+watch(
+  () => props.CheckedList,
+  (current) => {
+    CheckedList.value = current
+  }
+)
+
+const emit = defineEmits(['handleCloseRadio'])
+
+const handleClose = (tag: any) => {
+  CheckedList.value?.splice(CheckedList.value.indexOf(tag), 1)
+  emit('handleCloseRadio', tag)
+}
+</script>
+
+<template>
+  <div>
+    <el-card class="Rules_Card" shadow="never">
+      <div class="Card_Title">{{ props.Title }}</div>
+      <div class="right_flex">
+        <el-tag
+          class="tag"
+          v-for="(tag, index) in CheckedList"
+          :key="index"
+          closable
+          @close="handleClose(tag)"
+        >
+          {{ tag }}
+        </el-tag>
+      </div>
+    </el-card>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.tag {
+  border-radius: 3px;
+  margin-bottom: 4px;
+  color: var(--color-neutral-1);
+  height: 32px;
+  width: fit-content;
+  font-weight: 400;
+  font-size: 14px;
+  background-color: var(--tag-bg-color) !important;
+  border-color: var(--tag-bg-color);
+}
+.tag:last-child {
+  margin-bottom: 0;
+}
+:deep(.el-card__body) {
+  padding: 8px !important;
+  max-height: 400px;
+  overflow-y: scroll;
+  background-color: var(--color-system-card-bg);
+}
+:deep(.el-tag .el-tag__close svg) {
+  width: 16px !important;
+}
+.right_flex {
+  display: flex;
+  flex-direction: column;
+}
+.Rules_Card {
+  margin: 8px;
+}
+.Card_Title {
+  color: var(--color-neutral-1);
+  font-size: 14px;
+  font-weight: 600;
+  margin: 9px 0 11px 0;
+}
+</style>

+ 376 - 0
src/components/CreateAddRules/src/components/DelayedType.vue

@@ -0,0 +1,376 @@
+<script lang="ts" setup>
+import { ref, computed, watch } from 'vue'
+
+const props = defineProps({
+  Title: String,
+  DelayedData: Object
+})
+const delayed_data = ref(props.DelayedData)
+watch(
+  () => props.DelayedData,
+  (current) => {
+    delayed_data.value = current
+    DelayedInit()
+  }
+)
+
+const DelayedInit = () => {
+  let array2: any = []
+  OceanCheckedList.value = []
+  if (delayed_data.value?.atd_etd != '' && delayed_data.value?.atd_etd != undefined) {
+    isDeparture.value = true
+    DepartureTime.value = delayed_data.value?.atd_etd
+    DepartureSelect.value = delayed_data.value?.atd_etd_unit
+    OceanCheckedList.value.push('Departure Delayed')
+    array2.push('Departure')
+  }
+  if (delayed_data.value?.ata_eta != '' && delayed_data.value?.ata_eta != undefined) {
+    isArrival.value = true
+    ArrivalTime.value = delayed_data.value?.ata_eta
+    ArrivalSelect.value = delayed_data.value?.ata_eta_unit
+    OceanCheckedList.value.push('Arrival Delayed (ATA-ETA)')
+    array2.push('Arrival')
+  }
+  CheckChange(OceanCheckedList.value)
+  changedeparture(array2)
+}
+const isDeparture = ref(false)
+const isArrival = ref(false)
+const OceanActive = ref(['DelayedShipments'])
+const OceanCheckedList = ref()
+const DepartureTime = ref()
+const DepartureSelect = ref('')
+const ArrivalTime = ref()
+const ArrivalSelect = ref('')
+const emit = defineEmits(['ChangeCheckRules', 'closeDelayed'])
+const DepartureList = ref({
+  Departure: '',
+  Arrival: ''
+})
+let Departurestr: any = ''
+let Arrivalstr: any = ''
+
+const clampedValue = computed({
+  get: () => DepartureTime.value,
+  set: (newVal) => {
+    const isValid = /^[1-9]\d*$/.test(newVal);
+    DepartureTime.value = isValid ? newVal : '';
+    if( DepartureTime.value!='') {
+      DepartureTime.value = Math.min(Math.max(parseInt(DepartureTime.value, 10), 1), 365)
+    }
+  }
+})
+const clampedArrivalValue = computed({
+  get: () => ArrivalTime.value,
+  set: (newVal) => {
+    const isValid = /^[1-9]\d*$/.test(newVal);
+    ArrivalTime.value = isValid ? newVal : '';
+    if( ArrivalTime.value!='') {
+      ArrivalTime.value = Math.min(Math.max(parseInt(ArrivalTime.value, 10), 1), 365)
+    }
+  }
+})
+
+const CheckChange = (val: any) => {
+  if (val.includes('Departure Delayed')) {
+    isDeparture.value = true
+    if(clampedValue.value != '' && clampedValue.value!= undefined) {
+      Departurestr = 'Departure Delayed (ATD-ETD)' + ' ≥ ' + clampedValue.value + ' ' + DepartureSelect.value
+    } else {
+      Departurestr = ''
+    }
+    if (DepartureSelect.value != '') {
+      DepartureList.value.Departure = Departurestr
+    }
+    if (val.includes('Arrival Delayed (ATA-ETA)')) {
+      isArrival.value = true
+      if(clampedArrivalValue.value != '' && clampedArrivalValue.value!= undefined) {
+        Arrivalstr = 'Arrival Delayed (ATA-ETA)' + ' ≥ ' + clampedArrivalValue.value + ' ' + ArrivalSelect.value
+      } else {
+        Arrivalstr = ''
+      }
+      if (ArrivalSelect.value != '') {
+        DepartureList.value.Arrival = Arrivalstr
+      }
+    } else {
+      isArrival.value = false
+      DepartureList.value.Arrival = ''
+      clampedArrivalValue.value = ''
+      ArrivalSelect.value = ''
+    }
+  } else {
+    isDeparture.value = false
+    DepartureList.value.Departure = ''
+    clampedValue.value = ''
+    DepartureSelect.value = ''
+    if (val.includes('Arrival Delayed (ATA-ETA)')) {
+      isArrival.value = true
+      if(clampedArrivalValue.value != '' && clampedArrivalValue.value!= undefined) {
+        Arrivalstr = 'Arrival Delayed (ATA-ETA)' + ' ≥ ' + clampedArrivalValue.value + ' ' + ArrivalSelect.value
+      } else {
+        Arrivalstr = ''
+      }
+      if (ArrivalSelect.value != '') {
+        DepartureList.value.Arrival = Arrivalstr
+      }
+    } else {
+      isArrival.value = false
+      DepartureList.value.Arrival = ''
+      clampedArrivalValue.value = ''
+      ArrivalSelect.value = ''
+    }
+  }
+  emit('ChangeCheckRules', DepartureList.value)
+}
+const handleCheckboxClick = (event:any) => {
+  // 判断点击的是否为复选框输入区域
+  const isCheckboxInput = event.target.closest('.el-checkbox__inner')
+  const isCheckboxTitle = event.target.closest('.titlecheckbox')
+  if (!isCheckboxInput) {
+    // 阻止默认切换行为
+    if(!isCheckboxTitle) {
+      event.preventDefault()
+    }
+  }
+}
+const changedeparture = (val: any) => {
+  if(props.Title != 'Air Shipments') {
+    DepartureSelect.value = 'Day(s)'
+    ArrivalSelect.value = 'Day(s)'
+  }
+  if (val == 'Departure') {
+    if(clampedValue.value != '' && clampedValue.value!= undefined) {
+      Departurestr = 'Departure Delayed (ATD-ETD)' + ' ≥ ' + clampedValue.value + ' ' + DepartureSelect.value
+    } else {
+      Departurestr = ''
+    }
+    if (DepartureSelect.value != '') {
+      DepartureList.value.Departure = Departurestr
+    }
+    if (val == 'Arrival') {
+      if(clampedArrivalValue.value != '' && clampedArrivalValue.value!= undefined) {
+        Arrivalstr = 'Arrival Delayed (ATA-ETA)' + ' ≥ ' + clampedArrivalValue.value + ' ' + ArrivalSelect.value
+      } else {
+        Arrivalstr = ''
+      }
+      if (ArrivalSelect.value != '') {
+        DepartureList.value.Arrival = Arrivalstr
+      }
+    }
+  } else {
+    if (val == 'Arrival') {
+      if(clampedArrivalValue.value != '' && clampedArrivalValue.value!= undefined) {
+        Arrivalstr = 'Arrival Delayed (ATA-ETA)' + ' ≥ ' + clampedArrivalValue.value + ' ' + ArrivalSelect.value
+      } else {
+        Arrivalstr = ''
+      }
+      if (ArrivalSelect.value != '') {
+        DepartureList.value.Arrival = Arrivalstr
+      }
+    }
+  }
+  emit('ChangeCheckRules', DepartureList.value)
+}
+const closeDelayed = (val: any) => {
+  if (val.includes('Departure')) {
+    OceanCheckedList.value.splice(OceanCheckedList.value.indexOf('Departure Delayed (ATD-ETD)'), 1)
+    DepartureList.value.Departure = ''
+    isDeparture.value = false
+    DepartureTime.value = ''
+    DepartureSelect.value = ''
+  }
+  if (val.includes('Arrival')) {
+    OceanCheckedList.value.splice(OceanCheckedList.value.indexOf('Arrival Delayed (ATA-ETA)'), 1)
+    DepartureList.value.Arrival = ''
+    isArrival.value = false
+    ArrivalTime.value = ''
+    ArrivalSelect.value = ''
+  }
+  emit('ChangeCheckRules', DepartureList.value)
+}
+
+// 清除所有数据
+const ClearData = () => {
+  OceanCheckedList.value = []
+  DepartureList.value.Departure = ''
+  isDeparture.value = false
+  DepartureList.value.Arrival = ''
+  isArrival.value = false
+  DepartureTime.value = ''
+  DepartureSelect.value = ''
+  ArrivalTime.value = ''
+  ArrivalSelect.value = ''
+}
+
+defineExpose({
+  closeDelayed,
+  ClearData
+})
+</script>
+<template>
+  <div class="Ocean_collapse">
+    <el-collapse v-model="OceanActive">
+      <el-collapse-item name="DelayedShipments">
+        <template #title>
+          <div class="Rules_Title OceanTitle">{{ props.Title }}</div>
+        </template>
+        <div class="oceanCheckbox">
+          <el-checkbox-group @change="CheckChange" v-model="OceanCheckedList">
+            <el-checkbox @click="handleCheckboxClick($event)"  class="delayedType" value="Departure Delayed">
+              <div class="titlecheckbox">Departure Delayed (ATD-ETD)</div>
+              <div v-if="isDeparture" class="flex" style="margin-top: 16px">
+                <span class="delayedTitle">Delayed Time</span>
+                <span class="delayedIcon">≥</span>
+                <el-input
+                  v-model="clampedValue"
+                  class="input-with-select"
+                  @input="changedeparture('Departure')"
+                  v-if="props.Title == 'Air Shipments'" 
+                >
+                  <template #append>
+                    <el-select
+                      v-model="DepartureSelect"
+                      placeholder="Select"
+                      class="arrivalselect"
+                      @change="changedeparture('Departure')"
+                    >
+                      <el-option label="Day(s)" value="Day(s)" />
+                      <el-option label="Hour(s)" value="Hour(s)" />
+                    </el-select>
+                  </template>
+                </el-input>
+                <el-input
+                  v-else
+                  v-model="clampedValue"
+                  @input="changedeparture('Departure')"
+                  class="input-with-select1"
+                >
+                </el-input>
+                <div
+                v-if="props.Title != 'Air Shipments'" class="Days">Day(s)</div>
+              </div>
+            </el-checkbox>
+            <el-checkbox @click="handleCheckboxClick($event)"  class="delayedType" value="Arrival Delayed (ATA-ETA)">
+              <div class="titlecheckbox">Arrival Delayed (ATA-ETA)</div>
+              <div v-if="isArrival" class="flex" style="margin-top: 16px">
+                <span class="delayedTitle">Delayed Time</span>
+                <span class="delayedIcon">≥</span>
+                <el-input
+                  v-model="clampedArrivalValue"
+                  @input="changedeparture('Arrival')"
+                  class="input-with-select"
+                  v-if="props.Title == 'Air Shipments'"
+                >
+                  <template #append>
+                    <el-select
+                      v-model="ArrivalSelect"
+                      placeholder="Select"
+                      class="arrivalselect"
+                      @change="changedeparture('Arrival')"
+                    >
+                      <el-option label="Day(s)" value="Day(s)" />
+                      <el-option  label="Hour(s)" value="Hour(s)" />
+                    </el-select>
+                  </template>
+                </el-input>
+                <el-input
+                  v-else
+                  v-model="clampedArrivalValue"
+                  @input="changedeparture('Arrival')"
+                  class="input-with-select1"
+                >
+                </el-input>
+                <div v-if="props.Title != 'Air Shipments'" class="Days">Day(s)</div>
+              </div>
+            </el-checkbox>
+          </el-checkbox-group>
+        </div>
+      </el-collapse-item>
+    </el-collapse>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+:deep(.el-checkbox-group) {
+  border: 1px solid var(--color-system-border);
+  border-radius: 5px;
+}
+:deep(.el-checkbox) {
+  width: 100%;
+  background-color: var(--color-system-body-bg);
+  border-bottom: 1px solid var(--color-system-border);
+  padding: 8px;
+}
+:deep(.el-checkbox:first-child) {
+  border-radius: 6px 6px 0 0;
+}
+:deep(.el-checkbox:last-child) {
+  border-radius: 0 0 6px 6px;
+  border-bottom: none;
+}
+.delayedType {
+  align-items: start;
+  height: fit-content;
+  margin-right: 5px;
+  border-radius: 6px;
+  padding: 8px;
+}
+.input-with-select {
+  width: 180px;
+}
+.input-with-select1 {
+  width: 84px;
+  border-radius: 0;
+}
+:deep(.el-input__wrapper) {
+  width: 50%;
+  border-radius: 6px 0 0 6px;
+}
+:deep(.el-input-group__append) {
+  width: 50%;
+  padding: 0;
+  border: none;
+}
+:deep(.el-input-group--append .el-input-group__append .el-select .el-select__wrapper) {
+  padding: 5px 10px;
+  background-color: var(--color-system-body-bg);
+}
+.delayedTitle {
+  color: var(--color-neutral-2);
+  font-size: 12px;
+}
+.delayedIcon {
+  margin: 0 8px;
+}
+.oceanCheckbox {
+  margin-bottom: 8px;
+}
+:deep(.el-radio) {
+  align-items: center !important;
+}
+:deep(.el-input__wrapper) {
+  box-shadow: 0 0 0 1px var(--color-system-border-1) inset;
+}
+:deep(.el-input-group--append .el-input-group__append .el-select .el-select__wrapper) {
+  box-shadow: 0 1px 0 0 var(--color-system-border-1) inset,0 -1px 0 0 var(--color-system-border-1) inset,-1px 0 0 0 var(--color-system-border-1) inset;
+}
+.Days {
+  width: 84px;
+  border: 1px solid var(--color-system-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: 30px;
+}
+.flex {
+  display: flex;
+  align-items: center;
+}
+.titlecheckbox {
+  max-width: 198px;
+}
+</style>

+ 489 - 0
src/components/CreateAddRules/src/components/ETDShipments.vue

@@ -0,0 +1,489 @@
+<script lang="ts" setup>
+import { ref, computed, watch } from 'vue'
+
+const props = defineProps({
+  Title: String,
+  ETDData: Object
+})
+const ETD_data = ref(props.ETDData)
+watch(
+  () => props.ETDData,
+  (current) => {
+    ETD_data.value = current
+    ETDInit()
+  }
+)
+
+const ETDInit = () => {
+  OceanCheckedList.value = []
+  if (ETD_data.value?.ETDradio == 't') {
+    ETDRadio.value = '1'
+    OceanCheckedList.value.push('ETD')
+  } else {
+    if (ETD_data.value?.etd_old_sub_new != '' && ETD_data.value?.etd_old_sub_new != undefined) {
+      ETDRadio.value = '2'
+      isETD.value = true
+      ETDTime.value = ETD_data.value?.etd_old_sub_new
+      ETDSelect.value = ETD_data.value?.etd_old_sub_new_unit
+      OceanCheckedList.value.push('ETD')
+    }
+  }
+  if (ETD_data.value?.ETAradio == 't') {
+    ETARadio.value = '1'
+    OceanCheckedList.value.push('ETA')
+  } else {
+    if (ETD_data.value?.eta_old_sub_new != '' && ETD_data.value?.eta_old_sub_new != undefined) {
+      ETARadio.value = '2'
+      isETA.value = true
+      ETATime.value = ETD_data.value?.eta_old_sub_new
+      ETASelect.value = ETD_data.value?.eta_old_sub_new_unit
+      OceanCheckedList.value.push('ETA')
+    }
+  }
+  CheckChange(OceanCheckedList.value)
+  changeETDRadio(ETDRadio.value)
+  changeETARadio(ETARadio.value)
+}
+const isETD = ref(false)
+const isETA = ref(false)
+const OceanActive = ref(['ETDShipments'])
+const OceanCheckedList = ref()
+const ETDTime = ref()
+const ETDSelect = ref('')
+const ETATime = ref()
+const ETASelect = ref('')
+const ETDRadio = ref()
+const ETARadio = ref()
+const emit = defineEmits(['ChangeCheckRules', 'closeETD'])
+const ETDETAList = ref({
+  ETD: '',
+  ETA: ''
+})
+let ETDstr: any = ''
+let ETAstr: any = ''
+const CheckChange = (val: any) => {
+  if(props.Title != 'Air Shipments') {
+    ETASelect.value = 'Day(s)'
+    ETDSelect.value = 'Day(s)'
+  }
+  if (val.includes('ETD')) {
+    isETD.value = true
+    if (ETDSelect.value != ''  && clampedValue.value!= '' && clampedValue.value!= undefined) {
+      ETDETAList.value.ETD = ETDstr
+    } else {
+      ETDETAList.value.ETD = ''
+    }
+    if (val.includes('ETA')) {
+      isETA.value = true
+      if (ETASelect.value != ''  && clampedETAValue.value!= '' && clampedETAValue.value!= undefined) {
+        ETDETAList.value.ETA = ETAstr
+      } else {
+        ETDETAList.value.ETA = ''
+      }
+    } else {
+      isETA.value = false
+      ETDETAList.value.ETA = ''
+      clampedETAValue.value = ''
+      ETARadio.value = ''
+      ETASelect.value = ''
+      ETAstr = ''
+    }
+  } else {
+    isETD.value = false
+    ETDETAList.value.ETD = ''
+    clampedValue.value = ''
+    ETDRadio.value = ''
+    ETDSelect.value = ''
+    ETDstr = ''
+    if (val.includes('ETA')) {
+      isETA.value = true
+      if (ETASelect.value != ''  && clampedETAValue.value!= '' && clampedETAValue.value!= undefined) {
+        ETDETAList.value.ETA = ETAstr
+      } else {
+        ETDETAList.value.ETA = ''
+      }
+    } else {
+      isETA.value = false
+      ETDETAList.value.ETA = ''
+      clampedETAValue.value = ''
+      ETARadio.value = ''
+      ETASelect.value = ''
+      ETAstr = ''
+    }
+  }
+  emit('ChangeCheckRules', ETDETAList.value)
+}
+const changeETDRadio = (val: any) => {
+  if(props.Title != 'Air Shipments') {
+    ETDSelect.value = 'Day(s)'
+  }
+  if (val == 1) {
+    ETDstr = 'ETD: Notify for all changes'
+    ETDETAList.value.ETD = ETDstr
+  } else if (val == 2) {
+    ETDstr = 'ETD: Notify only when time difference ≥ ' + clampedValue.value + ' ' + ETDSelect.value
+    if (ETDSelect.value != '' && clampedValue.value!= '' && clampedValue.value!= undefined) {
+      ETDETAList.value.ETD = ETDstr
+    } else {
+      ETDETAList.value.ETD = ''
+    }
+  }
+  emit('ChangeCheckRules', ETDETAList.value)
+}
+const changeETARadio = (val: any) => {
+  if(props.Title != 'Air Shipments') {
+    ETASelect.value = 'Day(s)'
+  }
+  if (val == 1) {
+    ETAstr = 'ETA: Notify for all changes'
+    ETDETAList.value.ETA = ETAstr
+  } else if (val == 2) {
+    ETAstr = 'ETA: Notify only when time difference ≥ ' + clampedETAValue.value + ' ' + ETASelect.value
+    if (ETASelect.value != '' && clampedETAValue.value!= '' && clampedETAValue.value!= undefined) {
+      ETDETAList.value.ETA = ETAstr
+    } else {
+      ETDETAList.value.ETA = ''
+    }
+  }
+  emit('ChangeCheckRules', ETDETAList.value)
+}
+const changedeparture = (val: any) => {
+  if(props.Title != 'Air Shipments') {
+    ETDSelect.value = 'Day(s)'
+    ETASelect.value = 'Day(s)'
+  }
+  if (val == 'ETD') {
+    // if(ETDstr.includes('all changes')) {
+    //   ETDETAList.value.ETD = ETDstr
+    // } else {
+    //   ETDstr = 'ETD: Notify only when time difference ≥ ' + clampedValue.value + ' ' + ETDSelect.value
+    //   if (ETDSelect.value != '' && clampedValue.value!= '' && clampedValue.value!= undefined) {
+    //     ETDRadio.value = '2'
+    //     ETDETAList.value.ETD = ETDstr
+    //   } else {
+    //     ETDETAList.value.ETD = ''
+    //   }
+    ETDstr = 'ETD: Notify only when time difference ≥ ' + clampedValue.value + ' ' + ETDSelect.value
+    ETDRadio.value = '2'
+    if (ETDSelect.value != '' && clampedValue.value!= '' && clampedValue.value!= undefined) {
+      ETDETAList.value.ETD = ETDstr
+    } else {
+      ETDETAList.value.ETD = ''
+    }
+    if (val == 'ETA') {
+      // if(ETAstr.includes('all changes')) {
+      //   ETDETAList.value.ETA = ETAstr
+      // } else {
+      //   ETAstr = 'ETA: Notify only when time difference ≥ ' + clampedETAValue.value + ' ' + ETASelect.value
+      //   if (ETASelect.value != '' && clampedETAValue.value!= '' && clampedETAValue.value!= undefined) {
+      //     ETARadio.value = '2'
+      //     ETDETAList.value.ETA = ETAstr
+      //   } else {
+      //     ETDETAList.value.ETA = ''
+      //   }
+      // }
+      ETAstr = 'ETA: Notify only when time difference ≥ ' + clampedETAValue.value + ' ' + ETASelect.value
+      ETARadio.value = '2'
+      if (ETASelect.value != '' && clampedETAValue.value!= '' && clampedETAValue.value!= undefined) {
+        ETDETAList.value.ETA = ETAstr
+      } else {
+        ETDETAList.value.ETA = ''
+      }
+    }
+  } else {
+    // if(ETAstr.includes('all changes')) {
+    //   ETDETAList.value.ETA = ETAstr
+    // } else {
+    //   ETAstr = 'ETA: Notify only when time difference ≥ ' + clampedETAValue.value + ' ' + ETASelect.value
+    //   if (ETASelect.value != '' && clampedETAValue.value!= '' && clampedETAValue.value!= undefined) {
+    //     ETARadio.value = '2'
+    //     ETDETAList.value.ETA = ETAstr
+    //   } else {
+    //     ETDETAList.value.ETA = ''
+    //   }
+    // }
+    ETAstr = 'ETA: Notify only when time difference ≥ ' + clampedETAValue.value + ' ' + ETASelect.value
+    ETARadio.value = '2'
+    if (ETASelect.value != '' && clampedETAValue.value!= '' && clampedETAValue.value!= undefined) {
+      ETDETAList.value.ETA = ETAstr
+    } else {
+      ETDETAList.value.ETA = ''
+    }
+  }
+  emit('ChangeCheckRules', ETDETAList.value)
+}
+const handleCheckboxClick = (event:any) => {
+  // 判断点击的是否为复选框输入区域
+  const isCheckboxInput = event.target.closest('.el-checkbox__inner')
+  const isCheckboxTitle = event.target.closest('.titlecheckbox')
+  const isCheckboxRadio = event.target.closest('.radiocheckbox')
+  if (!isCheckboxInput) {
+    // 阻止默认切换行为
+    if(!isCheckboxTitle) {
+      if(!isCheckboxRadio) {
+        event.preventDefault()
+      }
+    }
+  }
+}
+const closeETD = (val: any) => {
+  if (val.includes('ETD')) {
+    OceanCheckedList.value.splice(OceanCheckedList.value.indexOf('ETD'), 1)
+    ETDETAList.value.ETD = ''
+    isETD.value = false
+    ETDTime.value = ''
+    ETDSelect.value = ''
+    ETDRadio.value = 0
+  }
+  if (val.includes('ETA')) {
+    OceanCheckedList.value.splice(OceanCheckedList.value.indexOf('ETA'), 1)
+    ETDETAList.value.ETA = ''
+    isETA.value = false
+    ETATime.value = ''
+    ETASelect.value = ''
+    ETARadio.value = 0
+  }
+  emit('ChangeCheckRules', ETDETAList.value)
+}
+
+// 清除所有数据
+const ClearData = () => {
+  OceanCheckedList.value = []
+  ETDETAList.value.ETD = ''
+  isETD.value = false
+  ETDETAList.value.ETA = ''
+  isETA.value = false
+  ETDRadio.value = 0
+  ETARadio.value = 0
+  ETDTime.value = ''
+  ETDSelect.value = ''
+  ETATime.value = ''
+  ETASelect.value = ''
+}
+
+defineExpose({
+  closeETD,
+  ClearData
+})
+
+const clampedValue = computed({
+  get: () => ETDTime.value,
+  set: (newVal) => {
+    const isValid = /^[1-9]\d*$/.test(newVal);
+    ETDTime.value = isValid ? newVal : '';
+    if( ETDTime.value!='') {
+      ETDTime.value = Math.min(Math.max(parseInt(ETDTime.value, 10), 1), 365)
+    }
+  }
+})
+const clampedETAValue = computed({
+  get: () => ETATime.value,
+  set: (newVal) => {
+    const isValid = /^[1-9]\d*$/.test(newVal);
+    ETATime.value = isValid ? newVal : '';
+    if( ETATime.value!='') {
+      ETATime.value = Math.min(Math.max(parseInt(ETATime.value, 10), 1), 365)
+    }
+  }
+})
+</script>
+<template>
+  <div class="Ocean_collapse">
+    <el-collapse v-model="OceanActive">
+      <el-collapse-item name="ETDShipments">
+        <template #title>
+          <div class="Rules_Title OceanTitle">{{ props.Title }}</div>
+        </template>
+        <div class="oceanCheckbox">
+          <el-checkbox-group @change="CheckChange" v-model="OceanCheckedList">
+            <el-checkbox @click="handleCheckboxClick($event)" class="delayedType" value="ETD">
+              <div class="titlecheckbox">ETD</div>
+              <div v-if="isETD" style="margin-top: 16px">
+                <el-radio-group class="radiocheckbox" v-model="ETDRadio" @change="changeETDRadio">
+                  <el-radio value="1">Notify for all changes</el-radio>
+                  <el-radio value="2">
+                    <div class="flex">
+                      Notify only when time difference
+                      <span class="delayedIcon">≥</span>
+                      <el-input
+                        v-if="props.Title == 'Air Shipments'"
+                        v-model="clampedValue"
+                        class="input-with-select"
+                        @input="changedeparture('ETD')"
+                      >
+                        <template #append>
+                          <el-select
+                            v-model="ETDSelect"
+                            placeholder="Select"
+                            class="arrivalselect"
+                            @change="changedeparture('ETD')"
+                          >
+                            <el-option label="Day(s)" value="Day(s)" />
+                            <el-option label="Hour(s)" value="Hour(s)" />
+                          </el-select>
+                        </template>
+                      </el-input>
+                      <el-input
+                        v-else
+                        v-model="clampedValue"
+                        @input="changedeparture('ETD')"
+                        class="input-with-select1"
+                      >
+                      </el-input>
+                      <div
+                      v-if="props.Title != 'Air Shipments'" class="Days">Day(s)</div>
+                    </div>
+                  </el-radio>
+                </el-radio-group>
+              </div>
+            </el-checkbox>
+            <el-checkbox class="delayedType" @click="handleCheckboxClick($event)"  value="ETA">
+              <div class="titlecheckbox">ETA</div>
+              <div v-if="isETA" style="margin-top: 16px">
+                <el-radio-group class="radiocheckbox" v-model="ETARadio" @change="changeETARadio">
+                  <el-radio value="1">Notify for all changes</el-radio>
+                  <el-radio value="2">
+                    <div class="flex">
+                      Notify only when time difference
+                      <span class="delayedIcon">≥</span>
+                      <el-input
+                      v-if="props.Title == 'Air Shipments'"
+                        v-model="clampedETAValue"
+                        class="input-with-select"
+                        @input="changedeparture('ETA')"
+                      >
+                        <template #append>
+                          <el-select
+                            v-model="ETASelect"
+                            placeholder="Select"
+                            class="arrivalselect"
+                            @change="changedeparture('ETA')"
+                          >
+                            <el-option label="Day(s)" value="Day(s)" />
+                            <el-option label="Hour(s)" value="Hour(s)" />
+                          </el-select>
+                        </template>
+                      </el-input>
+                      <el-input
+                        v-else
+                        v-model="clampedETAValue"
+                        @input="changedeparture('ETA')"
+                        class="input-with-select1"
+                      >
+                      </el-input>
+                      <div
+                      v-if="props.Title != 'Air Shipments'" class="Days">Day(s)</div>
+                    </div>
+                  </el-radio>
+                </el-radio-group>
+              </div>
+            </el-checkbox>
+          </el-checkbox-group>
+        </div>
+      </el-collapse-item>
+    </el-collapse>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+:deep(.el-checkbox-group) {
+  border: 1px solid var(--color-system-border);
+  border-radius: 5px;
+}
+:deep(.el-checkbox) {
+  width: 100%;
+  background-color: var(--color-system-body-bg);
+  border-bottom: 1px solid var(--color-system-border);
+  padding: 8px;
+}
+:deep(.el-checkbox:first-child) {
+  border-radius: 6px 6px 0 0;
+}
+:deep(.el-checkbox:last-child) {
+  border-radius: 0 0 6px 6px;
+  border-bottom: none;
+}
+:deep(.el-radio) {
+  border: none !important;
+}
+:deep( .el-radio__inner) {
+  border: 1px solid var(--color-system-checkbox-border);
+}
+.delayedType {
+  align-items: start;
+  height: fit-content;
+  margin-right: 5px;
+  border-radius: 6px;
+  padding: 8px;
+}
+.input-with-select {
+  width: 180px;
+}
+.input-with-select1 {
+  width: 84px;
+  border-radius: 0;
+}
+:deep(.el-input__wrapper) {
+  width: 50%;
+  border-radius: 6px 0 0 6px;
+}
+:deep(.el-input-group__append) {
+  width: 50%;
+  padding: 0;
+  border: none;
+}
+:deep(.el-input-group--append .el-input-group__append .el-select .el-select__wrapper) {
+  padding: 5px 10px;
+  background-color: var(--color-system-body-bg);
+}
+.delayedIcon {
+  margin: 0 8px;
+}
+.oceanCheckbox {
+  margin-bottom: 8px;
+}
+: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;
+  line-height: 32px;
+  align-items: center !important;
+}
+:deep(.el-radio__input.is-checked + .el-radio__label) {
+  color: var(--color-neutral-1);
+}
+:deep(.el-input__wrapper) {
+  box-shadow: 0 0 0 1px var(--color-system-border-1) inset;
+}
+:deep(.el-input-group--append .el-input-group__append .el-select .el-select__wrapper) {
+  box-shadow: 0 1px 0 0 var(--color-system-border-1) inset,0 -1px 0 0 var(--color-system-border-1) inset,-1px 0 0 0 var(--color-system-border-1) inset;
+}
+.Days {
+  width: 84px;
+  border: 1px solid var(--color-system-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: 30px;
+}
+.flex {
+  display: flex;
+  align-items: center;
+}
+.titlecheckbox {
+  max-width: 28px;
+}
+</style>

+ 416 - 0
src/components/CreateAddRules/src/components/NotiFrequency.vue

@@ -0,0 +1,416 @@
+<script setup lang="ts">
+import { ref, watch } from 'vue'
+import moment from 'moment-timezone'
+import { defaultTimeZone } from '@/utils/timezone'
+
+const props = defineProps({
+  FrequencyData: Object
+})
+
+const frequency_data = ref(props.FrequencyData)
+const radio = ref(0)
+const FrequencyList = ref()
+const DailyTime = ref('')
+const WeeklyTime = ref('')
+const WeeklyDay = ref('')
+const WeekDay = ref([
+  {
+    label: 'Monday',
+    value: 'Monday'
+  },
+  {
+    label: 'Tuesday',
+    value: 'Tuesday'
+  },
+  {
+    label: 'Wednesday',
+    value: 'Wednesday'
+  },
+  {
+    label: 'Thursday',
+    value: 'Thursday'
+  },
+  {
+    label: 'Friday',
+    value: 'Friday'
+  },
+  {
+    label: 'Saturday',
+    value: 'Saturday'
+  },
+  {
+    label: 'Sunday',
+    value: 'Sunday'
+  }
+])
+const TimeZone = 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 TimeZoneDailySelect = ref()
+const TimeZoneWeeklySelect = ref()
+TimeZoneDailySelect.value = 'UTC' + moment().tz(moment.tz.guess()).format('Z')
+let defaultDailyTimeZone = TimeZoneDailySelect.value
+TimeZoneWeeklySelect.value = 'UTC' + moment().tz(moment.tz.guess()).format('Z')
+let defaultWeeklyTimeZone = TimeZoneDailySelect.value
+const isDaily = ref(false)
+const isWeekly = ref(false)
+let savesubscribeobj: any = {}
+
+watch(
+  () => props.FrequencyData,
+  (current) => {
+    frequency_data.value = current
+    FrequencyDataInit()
+  }
+)
+
+const emits = defineEmits(['ChangeFrequencyAdd'])
+
+// 初始设置Frequency
+const FrequencyDataInit = () => {
+  if (frequency_data.value?.frequency_type == 'Instant') {
+    radio.value = 1
+    ChangeFrequency(1)
+  } else if (frequency_data.value?.frequency_type == 'Daily') {
+    radio.value = 2
+    DailyTime.value = frequency_data.value?.daily_time
+    TimeZoneDailySelect.value = frequency_data.value?.daily_time_zone
+    ChangeFrequency(2)
+  } else if (frequency_data.value?.frequency_type == 'Weekly') {
+    radio.value = 3
+    WeeklyDay.value = frequency_data.value?.weekly_week
+    WeeklyTime.value = frequency_data.value?.weekly_time
+    TimeZoneWeeklySelect.value = frequency_data.value?.weekly_time_zone
+    ChangeFrequency(3)
+  } else {
+    radio.value = 0
+    ChangeFrequency(0)
+  }
+}
+
+// 更改Frequency时间
+const ChangeFrequency = (val: any) => {
+  FrequencyList.value = []
+  let str: any = ''
+  if (val == 1) {
+    isDaily.value = false
+    isWeekly.value = false
+    DailyTime.value = ''
+    WeeklyDay.value = ''
+    WeeklyTime.value = ''
+    TimeZoneDailySelect.value  = defaultDailyTimeZone
+    TimeZoneWeeklySelect.value  = defaultWeeklyTimeZone
+    str = 'Instant notification for each update'
+    FrequencyList.value.push(str)
+    savesubscribeobj.frequency_type = 'Instant'
+    savesubscribeobj.frequency_display = str
+  } else if (val == 2) {
+    isDaily.value = true
+    isWeekly.value = false
+    WeeklyDay.value = ''
+    WeeklyTime.value = ''
+    TimeZoneWeeklySelect.value  = defaultWeeklyTimeZone
+    str = 'Daily, ' + DailyTime.value + ', ' + TimeZoneDailySelect.value
+    if (DailyTime.value != '' && DailyTime.value != undefined && TimeZoneDailySelect.value !== '') {
+      FrequencyList.value.push(str)
+    }
+    savesubscribeobj.frequency_type = 'Daily'
+    savesubscribeobj.daily_time = DailyTime.value
+    savesubscribeobj.daily_time_zone = TimeZoneDailySelect.value
+    savesubscribeobj.frequency_display = str
+  } else if (val == 3) {
+    isDaily.value = false
+    isWeekly.value = true
+    DailyTime.value = ''
+    TimeZoneDailySelect.value  = defaultDailyTimeZone
+    str = 'Weekly, ' + WeeklyDay.value + ', ' + WeeklyTime.value + ', ' + TimeZoneWeeklySelect.value
+    if (WeeklyDay.value != '' && WeeklyTime.value != undefined && WeeklyTime.value != '' && TimeZoneWeeklySelect.value !== '') {
+      FrequencyList.value.push(str)
+    }
+    savesubscribeobj.frequency_type = 'Weekly'
+    if (WeeklyDay.value == 'Monday') {
+      savesubscribeobj.weekly_week = 1
+    } else if (WeeklyDay.value == 'Tuesday') {
+      savesubscribeobj.weekly_week = 2
+    } else if (WeeklyDay.value == 'Wednesday') {
+      savesubscribeobj.weekly_week = 3
+    } else if (WeeklyDay.value == 'Thursday') {
+      savesubscribeobj.weekly_week = 4
+    } else if (WeeklyDay.value == 'Friday') {
+      savesubscribeobj.weekly_week = 5
+    } else if (WeeklyDay.value == 'Saturday') {
+      savesubscribeobj.weekly_week = 6
+    } else if (WeeklyDay.value == 'Sunday') {
+      savesubscribeobj.weekly_week = 0
+    }
+    savesubscribeobj.weekly_time = WeeklyTime.value
+    savesubscribeobj.weekly_time_zone = TimeZoneWeeklySelect.value
+    savesubscribeobj.frequency_display = str
+  } else {
+    isDaily.value = false
+    isWeekly.value = false
+    DailyTime.value = ''
+    WeeklyTime.value = ''
+    WeeklyDay.value = ''
+    TimeZoneDailySelect.value  = defaultDailyTimeZone
+    TimeZoneWeeklySelect.value  = defaultWeeklyTimeZone
+    delete savesubscribeobj.frequency_type
+    delete savesubscribeobj.daily_time
+    delete savesubscribeobj.daily_time_zone
+    delete savesubscribeobj.weekly_week
+    delete savesubscribeobj.weekly_time
+    delete savesubscribeobj.weekly_time_zone
+  }
+  emits('ChangeFrequencyAdd', FrequencyList.value, savesubscribeobj)
+}
+const changeTime = (val: any) => {
+  if (val == 'Daily') {
+    ChangeFrequency(2)
+  } else {
+    ChangeFrequency(3)
+  }
+}
+
+// 删除 Frequency tag
+const handleCloseRadioFrequency = () => {
+  radio.value = 0
+  ChangeFrequency(0)
+}
+
+const user_type = localStorage.getItem('user_type')
+
+defineExpose({
+  handleCloseRadioFrequency,
+  FrequencyDataInit
+})
+</script>
+<template>
+  <div style="margin-top: 11px">
+    <el-radio-group v-model="radio" @change="ChangeFrequency">
+      <!-- <el-radio :value="1" v-if="user_type != null && user_type != 'customer'"
+        >Instant notification for each update</el-radio
+      > -->
+      <el-radio :value="2">
+        <div>Daily Summary</div>
+        <div class="Daily" v-if="isDaily">
+          <div class="Daily_left" style="margin-right: 8px">
+            Select Time
+            <div>
+              <el-time-select
+                v-model="DailyTime"
+                start="00:00"
+                step="00:30"
+                end="23:30"
+                prefix-icon=""
+                @change="changeTime('Daily')"
+                placeholder="Select Time"
+              ></el-time-select>
+            </div>
+          </div>
+          <div class="Daily_left">
+            Select Time Zone
+            <div>
+              <el-select
+                v-model="TimeZoneDailySelect"
+                placeholder="Select Time Zone"
+                @change="changeTime('Daily')"
+              >
+                <el-option
+                  v-for="item in TimeZone"
+                  :key="item.value"
+                  :label="item.label"
+                  :value="item.value"
+                />
+              </el-select>
+            </div>
+          </div>
+        </div>
+      </el-radio>
+      <el-radio :value="3">
+        <div>Weekly Summary</div>
+        <div class="Daily" v-if="isWeekly">
+          <div class="Weekly_left">
+            Select Day
+            <div>
+              <el-select
+                v-model="WeeklyDay"
+                @change="changeTime('Weekly')"
+                placeholder="Select Day"
+              >
+                <el-option
+                  v-for="item in WeekDay"
+                  :key="item.value"
+                  :label="item.label"
+                  :value="item.value"
+                />
+              </el-select>
+            </div>
+          </div>
+          <div class="Weekly_left" style="margin: 0 8px">
+            Select Time
+            <div>
+              <el-time-select
+                v-model="WeeklyTime"
+                @change="changeTime('Weekly')"
+                start="00:00"
+                step="00:30"
+                end="23:30"
+                prefix-icon=""
+                placeholder="Select time"
+              ></el-time-select>
+            </div>
+          </div>
+          <div class="Weekly_left">
+            Select Time Zone
+            <div>
+              <el-select
+                v-model="TimeZoneWeeklySelect"
+                placeholder="Select Time Zone"
+                @change="changeTime('Weekly')"
+              >
+                <el-option
+                  v-for="item in TimeZone"
+                  :key="item.value"
+                  :label="item.label"
+                  :value="item.value"
+                />
+              </el-select>
+            </div>
+          </div>
+        </div>
+      </el-radio>
+    </el-radio-group>
+  </div>
+</template>
+<style lang="scss" scoped>
+: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;
+  line-height: 32px;
+  align-items: start;
+}
+:deep( .el-radio__inner) {
+  border: 1px solid var(--color-system-checkbox-border);
+}
+:deep(.el-radio__input.is-checked + .el-radio__label) {
+  color: var(--color-neutral-1);
+}
+.Daily {
+  margin: 0 0 9px 0;
+  display: flex;
+}
+.Daily_left {
+  color: var(--color-neutral-2);
+  width: 50%;
+}
+.Weekly_left {
+  color: var(--color-neutral-2);
+  width: 33%;
+}
+:deep(.el-select__icon.el-icon svg) {
+  width: 1em !important;
+}
+:deep(.el-select__wrapper.is-filterable) {
+  padding-left: 7px;
+}
+:deep(.el-radio__inner) {
+  margin-top: 7px;
+}
+</style>

+ 100 - 0
src/components/CreateAddRules/src/components/NotiMethods.vue

@@ -0,0 +1,100 @@
+<script setup lang="ts">
+import { ref, watch } from 'vue'
+import Light_methods from '../images/illustration_system massage@2x.png'
+import Dark_methods from '../images/illustration_system massage_darkmode@2x.png'
+import { useThemeStore } from '@/stores/modules/theme'
+
+const themeStore = useThemeStore()
+
+const checkMethodList = ref()
+checkMethodList.value = []
+let savesubscribeobj: any = {}
+const props = defineProps({
+  MethodsData: Object
+})
+const MethodsImg = computed(() => {
+  return themeStore.theme === 'dark' ? Dark_methods : Light_methods
+})
+
+const methods_data = ref(props.MethodsData)
+watch(
+  () => props.MethodsData,
+  (current) => {
+    methods_data.value = current
+    MethodsInit()
+  }
+)
+const emits = defineEmits(['ChangeMethodsAdd'])
+// 初始设置Methods
+const MethodsInit = () => {
+  if (methods_data.value?.method_display != undefined) {
+    if (methods_data.value?.method_display.indexOf('Email') != -1) {
+      checkMethodList.value.push('By Email')
+    }
+  }
+  if (methods_data.value?.method_display != undefined) {
+    if (methods_data.value?.method_display.indexOf('System Message') != -1) {
+      checkMethodList.value.push('By System Message')
+    }
+  }
+  changeMethod(checkMethodList.value)
+}
+
+// 选中Method
+const changeMethod = (val: any) => {
+  if (val.indexOf('By Email') != -1) {
+    savesubscribeobj.method_by_email = true
+  } else {
+    savesubscribeobj.method_by_email = false
+  }
+  if (val.indexOf('By System Message') != -1) {
+    savesubscribeobj.method_by_message = true
+  } else {
+    savesubscribeobj.method_by_message = false
+  }
+  if (val.length != 0) {
+    savesubscribeobj.method_display = val.map((e: any) => e.replace('By ', '')).join(',')
+  } else {
+    savesubscribeobj.method_display = ''
+  }
+  emits('ChangeMethodsAdd', checkMethodList.value, savesubscribeobj)
+}
+const user_type = localStorage.getItem('user_type')
+</script>
+<template>
+  <div style="margin-top: 11px">
+    <div class="Method">
+      <el-checkbox-group v-model="checkMethodList" @change="changeMethod">
+        <el-checkbox
+          class="methodcheckbox"
+          value="By Email"
+          v-if="user_type != null && user_type != 'customer'"
+        >
+          <div>By Email</div>
+          <div class="methos_image"><img src="../images/illustration_email@2x.png" /></div>
+        </el-checkbox>
+        <el-checkbox class="methodcheckbox" value="By System Message">
+          <div>By System Message</div>
+          <div class="methos_image">
+            <img :src="MethodsImg" />
+          </div>
+        </el-checkbox>
+      </el-checkbox-group>
+    </div>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.methodcheckbox {
+  align-items: start;
+  height: fit-content;
+  width: 49%;
+  margin-right: 5px;
+  background-color: var(--color-system-notification-bg);
+  border-radius: 6px;
+  padding: 11px 0 0 13px;
+}
+.methos_image {
+  margin: 9px 0;
+}
+</style>

+ 100 - 0
src/components/CreateAddRules/src/components/RulesShipments.vue

@@ -0,0 +1,100 @@
+<script lang="ts" setup>
+import { ref, watch } from 'vue'
+
+interface OceanCheckboxItem {
+  value: string
+  label: string
+}
+interface Props {
+  CheckboxList: OceanCheckboxItem[]
+  Title: String
+  CheckedList: Array<''>
+}
+const props = defineProps<Props>()
+
+const OceanActive = ref(['OceanShipments'])
+const CheckedList = ref(props.CheckedList)
+const CheckboxList = ref(props.CheckboxList)
+watch(
+  () => props.CheckboxList,
+  (current) => {
+    CheckboxList.value = current
+  }
+)
+watch(
+  () => props.CheckedList,
+  (current) => {
+    CheckedList.value = current
+  }
+)
+
+const emit = defineEmits(['ChangeCheckRules'])
+const selectedLables = ref([])
+const CheckChange = (val: any) => {
+  selectedLables.value = []
+  // 遍历选中的value值,找到对应的label值
+  val.forEach((value) => {
+    const option = CheckboxList.value.find((item) => item.value === value)
+    if (option) {
+      selectedLables.value.push(option.label)
+    }
+  })
+  emit('ChangeCheckRules', selectedLables.value, CheckedList.value)
+}
+const hadleclose = (val:any) => {
+  selectedLables.value = []
+  // 遍历选中的value值,找到对应的code值
+  const option = CheckboxList.value.find((item) => item.label === val)
+  CheckedList.value = CheckedList.value.filter((item) => item !== option.value)
+}
+defineExpose({
+  hadleclose
+})
+</script>
+<template>
+  <div class="Ocean_collapse">
+    <el-collapse v-model="OceanActive">
+      <el-collapse-item name="OceanShipments">
+        <template #title>
+          <div class="Rules_Title OceanTitle">{{ props.Title }}</div>
+        </template>
+        <div class="oceanCheckbox">
+          <el-checkbox-group @change="CheckChange" v-model="CheckedList">
+            <el-checkbox
+              v-for="item in CheckboxList"
+              :key="item.label"
+              :label="item.label"
+              :value="item.value"
+            />
+          </el-checkbox-group>
+        </div>
+      </el-collapse-item>
+    </el-collapse>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+:deep(.el-checkbox-group) {
+  border: 1px solid var(--color-system-border);
+  border-radius: 5px;
+}
+:deep(.el-checkbox) {
+  width: 100%;
+  background-color: var(--color-system-body-bg);
+  border-bottom: 1px solid var(--color-system-border);
+  padding: 8px;
+}
+:deep(.el-checkbox:first-child) {
+  border-radius: 6px 6px 0 0;
+}
+:deep(.el-checkbox:last-child) {
+  border-radius: 0 0 6px 6px;
+  border-bottom: none;
+}
+.oceanCheckbox {
+  max-height: 321px;
+  overflow-x: hidden;
+  margin-bottom: 8px;
+  overflow-y: scroll;
+}
+</style>

+ 519 - 0
src/components/CreateAddRules/src/components/ShipmentRange.vue

@@ -0,0 +1,519 @@
+<script lang="ts" setup>
+import { ref, computed, watch } from 'vue'
+
+const OceanActive = ref(['TransportMode', 'Time'])
+const TransportCheckedList = ref([])
+interface OceanItem {
+  label: string
+  value: string
+}
+const TransportList = ref<OceanItem[]>([])
+const props = defineProps({
+  ShipmentRangeData: Object
+})
+const ShipmentRange_data = ref(props.ShipmentRangeData)
+TransportList.value = [
+  {
+    label: 'Ocean',
+    value: 'Ocean'
+  },
+  {
+    label: 'Air',
+    value: 'Air'
+  },
+  {
+    label: 'Road',
+    value: 'Road'
+  }
+]
+
+const TimeChecked = ref()
+const defaultradio = ref()
+const defaultradio2 = ref()
+
+watch(
+  () => props.ShipmentRangeData,
+  (current) => {
+    ShipmentRange_data.value = current
+    ShipmentRangeInit()
+  }
+)
+
+const ShipmentRangeInit = () => {
+  if (ShipmentRange_data.value?.shipment_transport_mode != undefined) {
+    TransportCheckedList.value = ShipmentRange_data.value?.shipment_transport_mode.split(';')
+    CheckChange(ShipmentRange_data.value?.shipment_transport_mode)
+  }
+  if (typeof ShipmentRange_data.value?.shipment_eta_limit == 'number' && typeof ShipmentRange_data.value?.shipment_eta_limit_from == 'number') {
+    ETATimeStart.value = ShipmentRange_data.value?.shipment_eta_limit_from
+    ETATimeEnd.value = ShipmentRange_data.value?.shipment_eta_limit
+    TimeChecked.value = 2
+    changeTime(2)
+  }
+  if (typeof ShipmentRange_data.value?.shipment_etd_limit == 'number' && typeof ShipmentRange_data.value?.shipment_etd_limit_from == 'number') {
+    ETDTimeStart.value = ShipmentRange_data.value?.shipment_etd_limit_from
+    ETDTimeEnd.value = ShipmentRange_data.value?.shipment_etd_limit
+    TimeChecked.value = 1
+    changeTime(1)
+  }
+}
+
+const ETDTimeStart = ref()
+const ETDTimeEnd = ref()
+const ETATimeStart = ref()
+const ETATimeEnd = ref()
+
+const clampedETDValueStart = computed({
+  get: () => ETDTimeStart.value,
+  set: (newVal) => {
+    // 转换为整数
+    const num = parseInt(newVal, 10);
+    // 处理非数字和NaN情况
+    if (isNaN(num)) {
+      ETDTimeStart.value = 0;
+      return;
+    }
+    // 范围限制
+    ETDTimeStart.value = Math.max(0, Math.min(365, num));
+  }
+})
+const clampedETDValueEnd = computed({
+  get: () => ETDTimeEnd.value,
+  set: (newVal) => {
+    // 转换为整数
+    const num = parseInt(newVal, 10);
+    // 处理非数字和NaN情况
+    if (isNaN(num)) {
+      ETDTimeEnd.value = 0;
+      return;
+    }
+    // 范围限制
+    ETDTimeEnd.value = Math.max(0, Math.min(365, num));
+  }
+})
+const clampedETAValueStart = computed({
+  get: () => ETATimeStart.value,
+  set: (newVal) => {
+    // 转换为整数
+    const num = parseInt(newVal, 10);
+    // 处理非数字和NaN情况
+    if (isNaN(num)) {
+      ETATimeStart.value = 0;
+      return;
+    }
+    // 范围限制
+    ETATimeStart.value = Math.max(0, Math.min(365, num));
+  }
+})
+const clampedETAValueEnd = computed({
+  get: () => ETATimeEnd.value,
+  set: (newVal) => {
+    // 转换为整数
+    const num = parseInt(newVal, 10);
+    // 处理非数字和NaN情况
+    if (isNaN(num)) {
+      ETATimeEnd.value = 0;
+      return;
+    }
+    // 范围限制
+    ETATimeEnd.value = Math.max(0, Math.min(365, num));
+  }
+})
+
+let Transportstr: any = ''
+let Timestr: any = ''
+const emit = defineEmits(['ChangeCheckRules', 'ChangeCheckTimeRules'])
+const CheckChange = (val: any) => {
+  if (val != '') {
+    Transportstr = 'Transport Mode: ' + val
+  } else {
+    Transportstr = ''
+  }
+  emit('ChangeCheckRules', Transportstr, TransportCheckedList.value)
+}
+
+const isETDVisible = ref(false)
+const isETAVisible = ref(false)
+// 输入ETD、ETA
+const changeTime = (val: any) => {
+  if (val == 1) {
+    isETDVisible.value = true
+    isETAVisible.value = false
+    defaultradio.value =''
+    defaultradio2.value =''
+    ETATimeStart.value = ''
+    ETATimeEnd.value = ''
+    if(typeof clampedETDValueStart.value == 'number' && typeof clampedETDValueEnd.value == 'number') {
+      if(clampedETDValueStart.value == 0 && clampedETDValueEnd.value == 30) {
+        defaultradio.value = 'Next 30 days'
+      } else if(clampedETDValueStart.value == 0 && clampedETDValueEnd.value == 60) {
+        defaultradio.value = 'Next 60 days'
+      } else if(clampedETDValueStart.value == 10 && clampedETDValueEnd.value == 60) {
+        defaultradio.value = 'Past 10 days to next 60 day'
+      } else if(clampedETDValueStart.value == 30 && clampedETDValueEnd.value == 0) {
+        defaultradio.value = 'Past 30 days'
+      } else {
+        defaultradio.value = 'Customize'
+      }
+      if(clampedETDValueStart.value == 0 && clampedETDValueEnd.value == 0) {
+        Timestr = ''
+      } else {
+        Timestr = 'ETD: minus ' + clampedETDValueStart.value + ' Day(s) to Plus '+ clampedETDValueEnd.value + ' Day(s)'
+      }
+    } else{
+      Timestr = ''
+    }
+    emit('ChangeCheckTimeRules', Timestr, clampedETDValueStart.value,clampedETDValueEnd.value)
+  } else if (val == 2) {
+    isETDVisible.value = false
+    isETAVisible.value = true
+    defaultradio2.value =''
+    defaultradio.value =''
+    ETDTimeStart.value = ''
+    ETDTimeEnd.value = ''
+    if(typeof clampedETAValueStart.value == 'number' && typeof clampedETAValueEnd.value == 'number') {
+      if(clampedETAValueStart.value == 0 && clampedETAValueEnd.value == 30) {
+        defaultradio2.value = 'Next 30 days'
+      } else if(clampedETAValueStart.value == 0 && clampedETAValueEnd.value == 60) {
+        defaultradio2.value = 'Next 60 days'
+      } else if(clampedETAValueStart.value == 10 && clampedETAValueEnd.value == 60) {
+        defaultradio2.value = 'Past 10 days to next 60 day'
+      } else if(clampedETAValueStart.value == 30 && clampedETAValueEnd.value == 0) {
+        defaultradio2.value = 'Past 30 days'
+      } else {
+        defaultradio2.value = 'Customize'
+      }
+      if(clampedETAValueStart.value == 0 && clampedETAValueEnd.value == 0) {
+        Timestr = ''
+      } else {
+        Timestr = 'ETA: minus ' + clampedETAValueStart.value + ' Day(s) to Plus '+ clampedETAValueEnd.value + ' Day(s)'
+      }
+    } else{
+      Timestr = ''
+    }
+    emit('ChangeCheckTimeRules', Timestr, clampedETAValueStart.value, clampedETAValueEnd.value)
+  } else {
+    Timestr = ''
+  }
+}
+
+//切换默认值
+const changedefaultradio = (val:any) => {
+  if(val == 'Next 30 days') {
+    clampedETDValueStart.value = 0
+    clampedETDValueEnd.value = 30
+  } else if(val == 'Next 60 days') {
+    clampedETDValueStart.value = 0
+    clampedETDValueEnd.value = 60
+  } else if(val == 'Past 30 days') {
+    clampedETDValueStart.value = 30
+    clampedETDValueEnd.value = 0
+  } else if(val == 'Past 10 days to next 60 days') {
+    clampedETDValueStart.value = 10
+    clampedETDValueEnd.value = 60
+  } else {
+    clampedETDValueStart.value = 0
+    clampedETDValueEnd.value = 0
+  }
+  if(clampedETDValueStart.value == 0 && clampedETDValueEnd.value == 0) {
+    Timestr = ''
+  } else {
+    Timestr = 'ETD: minus ' + clampedETDValueStart.value + ' Day(s) to Plus '+ clampedETDValueEnd.value + ' Day(s)'
+  }
+  emit('ChangeCheckTimeRules', Timestr, clampedETDValueStart.value,clampedETDValueEnd.value)
+}
+//切换默认值
+const changedefaultradioETA = (val:any) => {
+  if(val == 'Next 30 days') {
+    clampedETAValueStart.value = 0
+    clampedETAValueEnd.value = 30
+  } else if(val == 'Next 60 days') {
+    clampedETAValueStart.value = 0
+    clampedETAValueEnd.value = 60
+  } else if(val == 'Past 30 days') {
+    clampedETAValueStart.value = 30
+    clampedETAValueEnd.value = 0
+  } else if(val == 'Past 10 days to next 60 days') {
+    clampedETAValueStart.value = 10
+    clampedETAValueEnd.value = 60
+  } else {
+    clampedETAValueStart.value = 0
+    clampedETAValueEnd.value = 0
+  }
+  if(clampedETAValueStart.value == 0 && clampedETAValueEnd.value == 0) {
+    Timestr = ''
+  } else {
+    Timestr = 'ETA: minus ' + clampedETAValueStart.value + ' Day(s) to Plus '+ clampedETAValueEnd.value + ' Day(s)'
+  }
+  emit('ChangeCheckTimeRules', Timestr, clampedETAValueStart.value,clampedETAValueEnd.value)
+}
+const handleCloseCreateRule = (val: any) => {
+  if (val.indexOf('ETD') != -1 || val.indexOf('ETA') != -1) {
+    TimeChecked.value = 0
+    ETDTimeStart.value = ''
+    ETDTimeEnd.value = ''
+    isETDVisible.value = false
+    defaultradio.value = ''
+    defaultradio2.value = ''
+    ETATimeStart.value = ''
+    ETATimeEnd.value = ''
+    isETAVisible.value = false
+  } else if (val.indexOf('Transport') != -1) {
+    TransportCheckedList.value = []
+  }
+}
+
+defineExpose({
+  handleCloseCreateRule
+})
+</script>
+<template>
+  <div class="Ocean_collapse">
+    <el-collapse v-model="OceanActive">
+      <el-collapse-item name="TransportMode">
+        <template #title>
+          <span class="stars_red">*</span><div class="Rules_Title OceanTitle">Transport Mode</div>
+        </template>
+        <div class="oceanCheckbox">
+          <el-checkbox-group @change="CheckChange" v-model="TransportCheckedList">
+            <el-checkbox
+              v-for="item in TransportList"
+              :key="item.label"
+              :label="item.label"
+              :value="item.value"
+            />
+          </el-checkbox-group>
+        </div>
+      </el-collapse-item>
+      <el-collapse-item name="Time">
+        <template #title>
+          <span class="stars_red">*</span><div class="Rules_Title OceanTitle">Time</div>
+        </template>
+        <div class="oceanCheckbox">
+          <el-radio-group v-model="TimeChecked" @change="changeTime">
+            <el-radio :value="1">
+              <div>ETD</div>
+              <div v-if="isETDVisible" class="oceanCheckbox2">
+                <el-radio-group v-model="defaultradio" @change="changedefaultradio">
+                  <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: 0 8px 8px 0;flex-wrap: wrap;">
+                  <div class="date_flex">
+                    <div class="time_title">Start Date</div>
+                    <div class="flex">
+                      <div class="currentTime">Current time minus</div>
+                      <el-input
+                        @input="changeTime('1')"
+                        v-model="clampedETDValueStart"
+                        class="input-with-select"
+                      >
+                      </el-input>
+                      <div class="Days">Day(s)</div>
+                    </div>
+                  </div>
+                  <div class="To">To</div>
+                  <div class="date_flex">
+                    <div class="time_title">End Date</div>
+                    <div class="flex">
+                      <div class="currentTime">Current time plus</div>
+                      <el-input
+                        @input="changeTime('1')"
+                        v-model="clampedETDValueEnd"
+                        class="input-with-select"
+                      >
+                      </el-input>
+                      <div class="Days">Day(s)</div>
+                    </div>
+                  </div>
+                </div>
+              </div>
+            </el-radio>
+            <el-radio :value="2">
+              <div>ETA</div>
+              <div v-if="isETAVisible" class="oceanCheckbox2">
+                <el-radio-group v-model="defaultradio2" @change="changedefaultradioETA">
+                  <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: 0 8px 8px 0;flex-wrap: wrap;">
+                  <div class="date_flex">
+                    <div class="time_title">Start Date</div>
+                    <div class="flex">
+                      <div class="currentTime">Current time minus</div>
+                      <el-input
+                        @input="changeTime('2')"
+                        v-model="clampedETAValueStart"
+                        class="input-with-select"
+                      >
+                      </el-input>
+                      <div class="Days">Day(s)</div>
+                    </div>
+                  </div>
+                  <div class="To">To</div>
+                  <div class="date_flex">
+                    <div class="time_title">End Date</div>
+                    <div class="flex">
+                      <div class="currentTime">Current time plus</div>
+                      <el-input
+                        @input="changeTime('2')"
+                        v-model="clampedETAValueEnd"
+                        class="input-with-select"
+                      >
+                      </el-input>
+                      <div class="Days">Day(s)</div>
+                    </div>
+                  </div>
+                </div>
+              </div>
+            </el-radio>
+          </el-radio-group>
+        </div>
+      </el-collapse-item>
+    </el-collapse>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+:deep(.el-checkbox-group) {
+  border: 1px solid var(--color-system-border);
+  border-radius: 5px;
+}
+:deep(.el-checkbox) {
+  width: 100%;
+  background-color: var(--color-system-body-bg);
+  border-bottom: 1px solid var(--color-system-border);
+  padding: 8px;
+}
+:deep(.el-checkbox:first-child) {
+  border-radius: 6px 6px 0 0;
+}
+:deep(.el-checkbox:last-child) {
+  border-radius: 0 0 6px 6px;
+  border-bottom: none;
+}
+.oceanCheckbox {
+  max-height: 321px;
+  overflow-x: hidden;
+  margin-bottom: 8px;
+  overflow-y: scroll;
+}
+.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);
+}
+.input-with-select {
+  border-radius: 0;
+}
+:deep(.el-input__wrapper) {
+  border-radius: 0;
+  opacity: 0.8;
+  height: 32px;
+}
+.flex {
+  display: flex;
+  align-items: center;
+}
+.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;
+  height: 25px;
+}
+.To {
+  color: var(--color-neutral-2);
+  font-size: 12px;
+  margin: 0 20px;
+  line-height: 24px;
+}
+:deep(.el-radio-group) {
+  display: block;
+}
+.oceanCheckbox2 {
+  :deep(.el-radio-group) {
+    display: flex;
+    flex-direction: row;
+}
+}
+: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;
+  line-height: 32px;
+  align-items:  start;
+}
+.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;
+    margin-right: 8px;
+  }
+}
+:deep( .el-radio__inner) {
+  border: 1px solid var(--color-system-checkbox-border);
+}
+:deep(.el-radio__input.is-checked + .el-radio__label) {
+  color: var(--color-neutral-1);
+}
+:deep(.el-radio__input) {
+  margin-top: 7px;
+}
+:deep(.el-input__wrapper) {
+  box-shadow: 0 0 0 1px var(--color-system-input-border) inset;
+}
+:deep(.el-input-group--append .el-input-group__append .el-select .el-select__wrapper) {
+  box-shadow: 0 1px 0 0 var(--color-system-input-border) inset,0 -1px 0 0 var(--color-system-border-1) inset,-1px 0 0 0 var(--color-system-border-1) inset;
+}
+:deep(.el-radio-button__inner) {
+  border: none;
+  height: 32px;
+  flex: 1;
+}
+:deep(.el-radio-button:first-child .el-radio-button__inner) {
+  border-left: none;
+}
+.stars_red {
+  color: var(--color-danger);
+}
+.date_flex {
+  flex: 1;
+}
+</style>

二進制
src/components/CreateAddRules/src/images/icon_collapse.png


二進制
src/components/CreateAddRules/src/images/icon_expand.png


二進制
src/components/CreateAddRules/src/images/icon_success_big@2x.png


二進制
src/components/CreateAddRules/src/images/illustration_email@2x.png


二進制
src/components/CreateAddRules/src/images/illustration_system massage@2x.png


二進制
src/components/CreateAddRules/src/images/illustration_system massage_darkmode@2x.png


二進制
src/components/CreateAddRules/src/images/submit_successful.png


+ 54 - 38
src/components/DateRange/src/DateRange.vue

@@ -3,8 +3,12 @@ import emitter from '@/utils/bus'
 import { ref, watch, onMounted, onBeforeMount } from 'vue'
 import IconDropDown from '@/components/IconDropDown'
 import CalendarDate from './components/CalendarDate.vue'
-import moment from 'moment'
 import dayjs from 'dayjs'
+import { useUserStore } from '@/stores/modules/user'
+
+const userStore = useUserStore()
+const formatDate = userStore.dateFormat
+const valueFormatDate = 'MM/DD/YYYY'
 
 onMounted(() => {
   defaultDate()
@@ -70,13 +74,15 @@ const defaultDate = () => {
       if (sessionStorage.getItem('searchTableQeuryTracking') == null) {
         DateStart.value = [dayjs().subtract(2, 'month').startOf('month'), dayjs().add(1, 'month')]
         daterangeObj.ETD =
-          DateStart.value[0].format('MMM/DD/YYYY') +
-          ' To ' +
-          DateStart.value[1].format('MMM/DD/YYYY')
+          DateStart.value[0].format(formatDate) + ' To ' + DateStart.value[1].format(formatDate)
         const obj = {
           title: 'ETD',
-          data: [DateStart.value[0].format('MM/DD/YYYY'), DateStart.value[1].format('MM/DD/YYYY')]
+          data: [
+            DateStart.value[0].format(valueFormatDate),
+            DateStart.value[1].format(valueFormatDate)
+          ]
         }
+
         daterangeObj2.ETD = obj
       } else {
         searchTableQeuryTracking.value =
@@ -87,12 +93,13 @@ const defaultDate = () => {
             dayjs(searchTableQeuryTracking.value.etd_end)
           ]
           daterangeObj.ETD =
-            DateStart.value[0].format('MMM/DD/YYYY') +
-            ' To ' +
-            DateStart.value[1].format('MMM/DD/YYYY')
+            DateStart.value[0].format(formatDate) + ' To ' + DateStart.value[1].format(formatDate)
           const obj = {
             title: 'ETD',
-            data: [DateStart.value[0].format('MM/DD/YYYY'), DateStart.value[1].format('MM/DD/YYYY')]
+            data: [
+              DateStart.value[0].format(valueFormatDate),
+              DateStart.value[1].format(valueFormatDate)
+            ]
           }
           daterangeObj2.ETD = obj
         }
@@ -102,10 +109,13 @@ const defaultDate = () => {
             dayjs(searchTableQeuryTracking.value.eta_end)
           ]
           daterangeObj.ETA =
-            DateEnd.value[0].format('MMM/DD/YYYY') + ' To ' + DateEnd.value[1].format('MMM/DD/YYYY')
+            DateEnd.value[0].format(formatDate) + ' To ' + DateEnd.value[1].format(formatDate)
           const obj = {
             title: 'ETA',
-            data: [DateEnd.value[0].format('MM/DD/YYYY'), DateEnd.value[1].format('MM/DD/YYYY')]
+            data: [
+              DateEnd.value[0].format(valueFormatDate),
+              DateEnd.value[1].format(valueFormatDate)
+            ]
           }
           daterangeObj2.ETA = obj
         }
@@ -115,14 +125,14 @@ const defaultDate = () => {
             dayjs(searchTableQeuryTracking.value.created_time_end)
           ]
           daterangeObj['Creation Time'] =
-            DateCreation.value[0].format('MMM/DD/YYYY') +
+            DateCreation.value[0].format(formatDate) +
             ' To ' +
-            DateCreation.value[1].format('MMM/DD/YYYY')
+            DateCreation.value[1].format(formatDate)
           const obj = {
             title: 'Creation Time',
             data: [
-              DateCreation.value[0].format('MM/DD/YYYY'),
-              DateCreation.value[1].format('MM/DD/YYYY')
+              DateCreation.value[0].format(valueFormatDate),
+              DateCreation.value[1].format(valueFormatDate)
             ]
           }
           daterangeObj2['Creation Time'] = obj
@@ -136,23 +146,25 @@ const defaultDate = () => {
       if (data.eta_start) {
         DateEnd.value = [dayjs(data.eta_start), dayjs(data.eta_end)]
         daterangeObj.ETA =
-          DateEnd.value[0].format('MMM/DD/YYYY') + ' To ' + DateEnd.value[1].format('MMM/DD/YYYY')
+          DateEnd.value[0].format(formatDate) + ' To ' + DateEnd.value[1].format(formatDate)
         const obj = {
           title: 'ETA',
-          data: [DateEnd.value[0].format('MM/DD/YYYY'), DateEnd.value[1].format('MM/DD/YYYY')]
+          data: [DateEnd.value[0].format(valueFormatDate), DateEnd.value[1].format(valueFormatDate)]
         }
+
         daterangeObj2.ETA = obj
         emit('defaultDate', daterangeObj, daterangeObj2, searchTableQeuryTracking.value)
       }
       if (data.etd_start) {
         DateStart.value = [dayjs(data.etd_start), dayjs(data.etd_end)]
         daterangeObj.ETD =
-          DateStart.value[0].format('MMM/DD/YYYY') +
-          ' To ' +
-          DateStart.value[1].format('MMM/DD/YYYY')
+          DateStart.value[0].format(formatDate) + ' To ' + DateStart.value[1].format(formatDate)
         const obj = {
           title: 'ETD',
-          data: [DateStart.value[0].format('MM/DD/YYYY'), DateStart.value[1].format('MM/DD/YYYY')]
+          data: [
+            DateStart.value[0].format(valueFormatDate),
+            DateStart.value[1].format(valueFormatDate)
+          ]
         }
         daterangeObj2.ETD = obj
         emit('defaultDate', daterangeObj, daterangeObj2, searchTableQeuryTracking.value)
@@ -162,10 +174,13 @@ const defaultDate = () => {
     if (sessionStorage.getItem('searchTableQeury') == null) {
       DateStart.value = [dayjs().subtract(2, 'month').startOf('month'), dayjs().add(1, 'month')]
       daterangeObj.ETD =
-        DateStart.value[0].format('MMM/DD/YYYY') + ' To ' + DateStart.value[1].format('MMM/DD/YYYY')
+        DateStart.value[0].format(formatDate) + ' To ' + DateStart.value[1].format(formatDate)
       const obj = {
         title: 'ETD',
-        data: [DateStart.value[0].format('MM/DD/YYYY'), DateStart.value[1].format('MM/DD/YYYY')]
+        data: [
+          DateStart.value[0].format(valueFormatDate),
+          DateStart.value[1].format(valueFormatDate)
+        ]
       }
       daterangeObj2.ETD = obj
     } else {
@@ -173,16 +188,17 @@ const defaultDate = () => {
         JSON.parse(sessionStorage.getItem('searchTableQeury') as string) || {}
       if (searchTableQeury.value.f_etd_start) {
         DateStart.value = [
-          dayjs(searchTableQeury.value.f_etd_start),
-          dayjs(searchTableQeury.value.f_etd_end)
+          dayjs(searchTableQeury.value.f_etd_start).format(valueFormatDate),
+          dayjs(searchTableQeury.value.f_etd_end).format(valueFormatDate)
         ]
         daterangeObj.ETD =
-          DateStart.value[0].format('MMM/DD/YYYY') +
-          ' To ' +
-          DateStart.value[1].format('MMM/DD/YYYY')
+          DateStart.value[0].format(formatDate) + ' To ' + DateStart.value[1].format(formatDate)
         const obj = {
           title: 'ETD',
-          data: [DateStart.value[0].format('MM/DD/YYYY'), DateStart.value[1].format('MM/DD/YYYY')]
+          data: [
+            DateStart.value[0].format(valueFormatDate),
+            DateStart.value[1].format(valueFormatDate)
+          ]
         }
         daterangeObj2.ETD = obj
       }
@@ -192,10 +208,10 @@ const defaultDate = () => {
           dayjs(searchTableQeury.value.m_eta_end)
         ]
         daterangeObj.ETA =
-          DateEnd.value[0].format('MMM/DD/YYYY') + ' To ' + DateEnd.value[1].format('MMM/DD/YYYY')
+          DateEnd.value[0].format(formatDate) + ' To ' + DateEnd.value[1].format(formatDate)
         const obj = {
           title: 'ETA',
-          data: [DateEnd.value[0].format('MM/DD/YYYY'), DateEnd.value[1].format('MM/DD/YYYY')]
+          data: [DateEnd.value[0].format(valueFormatDate), DateEnd.value[1].format(valueFormatDate)]
         }
         daterangeObj2.ETA = obj
       }
@@ -205,14 +221,14 @@ const defaultDate = () => {
           dayjs(searchTableQeury.value.created_time_end)
         ]
         daterangeObj['Creation Time'] =
-          DateCreation.value[0].format('MMM/DD/YYYY') +
+          DateCreation.value[0].format(formatDate) +
           ' To ' +
-          DateCreation.value[1].format('MMM/DD/YYYY')
+          DateCreation.value[1].format(formatDate)
         const obj = {
           title: 'Creation Time',
           data: [
-            DateCreation.value[0].format('MM/DD/YYYY'),
-            DateCreation.value[1].format('MM/DD/YYYY')
+            DateCreation.value[0].format(valueFormatDate),
+            DateCreation.value[1].format(valueFormatDate)
           ]
         }
         daterangeObj2['Creation Time'] = obj
@@ -226,8 +242,8 @@ daterangedata.value = []
 let daterangeObj2: any = {}
 const DateRangeChange = (val: any) => {
   if (val.data != null) {
-    const date1 = moment(String(val.data[0])).format('MMM-DD-YYYY')
-    const date2 = moment(String(val.data[1])).format('MMM-DD-YYYY')
+    const date1 = dayjs(String(val.data[0])).format(formatDate)
+    const date2 = dayjs(String(val.data[1])).format(formatDate)
     daterangeObj[val.title] = date1 + ' To ' + date2
     daterangeObj2[val.title] = val
   } else {
@@ -506,4 +522,4 @@ const clearDaterangeObj = () => {
 .Date_type {
   color: var(--color-neutral-2);
 }
-</style>
+</style>

+ 1 - 1
src/components/DateRange/src/components/CalendarDate.vue

@@ -125,7 +125,7 @@ const isTwoDate = (date: any) => {
       :disabled="Disabled"
       @change="changeRangeData"
       :placeholder="['Start Time', 'End Time']"
-      :format="userStore.dateFormat || 'MMM-DD-YYYY'"
+      :format="userStore.dateFormat"
       valueFormat="MM/DD/YYYY"
       @openChange="handleCalendarOpen(ETDDate)"
       @panelChange="handlePanelChange"

+ 9 - 7
src/components/DateRange/src/components/QuickCalendarDate.vue

@@ -1,8 +1,10 @@
 <script lang="ts" setup>
 import dayjs from 'dayjs'
 import { ref, watch, computed } from 'vue'
-import moment from 'moment'
+import { formatTimezone } from '@/utils/tools'
+import { useUserStore } from '@/stores/modules/user'
 
+const userStore = useUserStore()
 const props = defineProps({
   CalendarWidth: {
     type: String,
@@ -40,12 +42,12 @@ const ChangeToday = (val: any) => {
   if (val == 'Earliest') {
     // ETDDate.value = [dayjs(), dayjs()]
     ETDDate.value[0] = dayjs()
-    const date1 = moment(String(ETDDate.value[0])).format('MM/DD/YYYY')
+    const date1 = formatTimezone(String(ETDDate.value[0]))
     DateList.value[0] = date1
     daterange(DateList.value[1])
   } else {
     ETDDate.value[1] = dayjs()
-    const date1 = moment(String(ETDDate.value[1])).format('MM/DD/YYYY')
+    const date1 = formatTimezone(String(ETDDate.value[1]))
     DateList.value[1] = date1
     daterange(DateList.value[0])
   }
@@ -61,13 +63,13 @@ const handleCalendarOpen = (date: any) => {
 }
 const Earliest = () => {
   ETDDate.value[0] = dayjs('Oct-05-2009')
-  const date1 = moment(String(ETDDate.value[0])).format('MM/DD/YYYY')
+  const date1 = formatTimezone(String(ETDDate.value[0]))
   DateList.value[0] = date1
   daterange(DateList.value[1])
 }
 const Latest = () => {
   ETDDate.value[1] = dayjs()
-  const date1 = moment(String(ETDDate.value[1])).format('MM/DD/YYYY')
+  const date1 = formatTimezone(String(ETDDate.value[1]))
   DateList.value[1] = date1
   daterange(DateList.value[0])
 }
@@ -121,7 +123,7 @@ const placeholder = computed(() => {
       :disabled="props.isDisabled"
       @change="changeRangeData"
       :placeholder="placeholder"
-      format="MMM-DD-YYYY"
+      :format="userStore.dateFormat"
       valueFormat="MM/DD/YYYY"
       @openChange="handleCalendarOpen(ETDDate)"
       @panelChange="handlePanelChange"
@@ -181,4 +183,4 @@ const placeholder = computed(() => {
 :deep(.ant-picker-range) {
   padding: 4px 8px;
 }
-</style>
+</style>

+ 2 - 3
src/components/MoreFilters/src/MoreFilters.vue

@@ -898,7 +898,7 @@ const SearchMore = () => {
   if (AddDatePlaceType.value.length) {
     const isDateType = AddDatePlaceType.value.some((element) => element.placesType != '')
     if (isDateType) {
-      if (!isError.value && isError.value != undefined) {
+      if (isError.value && isError.value != undefined) {
         // 传数据给父组件
         emit('MoreFiltersSearch', MoreFiltersObj, MoreFiltersObj2)
         drawer.value = false
@@ -911,7 +911,7 @@ const SearchMore = () => {
   } else if (AddDateType.value.length) {
     const isDateType = AddDateType.value.some((element) => element.partyType != '')
     if (isDateType) {
-      if (!isError.value && isError.value != undefined) {
+      if (isError.value && isError.value != undefined) {
         // 传数据给父组件
         emit('MoreFiltersSearch', MoreFiltersObj, MoreFiltersObj2)
         drawer.value = false
@@ -1018,7 +1018,6 @@ import { useThemeStore } from '@/stores/modules/theme'
 const themeStore = useThemeStore()
 
 const moreFiltersGuideImg = computed(() => {
-  console.log('props.pageMode', props.pageMode)
   if (props.pageMode === 'tracking') {
     return themeStore.theme === 'dark' ? trackingMoreFiltersImgDark : trackingMoreFiltersImgLight
   } else {

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

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

+ 313 - 0
src/components/NotificationMessageCard/src/NotificationMessageCard.vue

@@ -0,0 +1,313 @@
+<script setup lang="ts">
+import EventCard from './components/EventCard.vue'
+import PasswordCard from './components/PasswordCard.vue'
+import FeatureUpdateCard from './components/FeatureUpdateCard.vue'
+import { useNotificationMessage } from '@/stores/modules/notificationMessage'
+import { cloneDeep } from 'lodash'
+// import { DynamicScroller, DynamicScrollerItem } from 'vue3-virtual-scroller'
+import 'vue3-virtual-scroller/dist/vue3-virtual-scroller.css'
+import { formatTimezone } from '@/utils/tools'
+
+const notificationMsgStore = useNotificationMessage()
+const props = withDefaults(
+  defineProps<{
+    data: any
+    isObserver?: boolean // 是否开启监听
+    isScrollPadding?: boolean // 是否有padding
+    updateReadCardsOnChange?: boolean // 是否在数据变化时请求接口更新已读卡片
+    topOffset?: number // 应减去高度
+    isShowInsertionTime?: boolean // 是否显示插入时间
+    isDrawer?: boolean // 是否在抽屉中使用
+  }>(),
+  {
+    isObserver: true,
+    isScrollPadding: false,
+    updateReadCardsOnChange: true,
+    topOffset: 220
+  }
+)
+const pageData = ref<any[]>([])
+
+const emit = defineEmits<{
+  seeAll: []
+  hasCardRead: []
+  viewMore: []
+  jumpTracking: []
+  loading: []
+}>()
+const handleSeeAll = () => {
+  emit('seeAll')
+}
+
+// 创建一个新的 IntersectionObserver 实例
+const observer = new IntersectionObserver(
+  (entries) => {
+    entries.forEach((entry) => {
+      const cardId = entry.target?.dataset?.cardId
+      if (entry.isIntersecting) {
+        // 将卡片设置为已经展示
+        notificationMsgStore.setReadCardMap(cardId)
+        const index = pageData.value.findIndex((item) => item.info.id === cardId)
+        if (index > -1) {
+          //  在1秒钟后将消息卡片设置为已读
+          setTimeout(() => {
+            pageData.value[index].info.isRead = true
+            emit('hasCardRead')
+          }, 400)
+        }
+      }
+    })
+  },
+  {
+    // 配置选项
+    root: null, // 使用视窗作为根元素
+    threshold: 0.1 // 当至少10%的元素进入视图时触发回调
+  }
+)
+const curPageAllCards = ref<any>([])
+// 监听元素是否在可视区域内
+const watchCards = () => {
+  curPageAllCards.value = document?.querySelectorAll('.notification-message-card')
+  curPageAllCards.value.forEach((card: any) => {
+    // const index = notificationMsgStore.notificationMsgList.indexOf(card.dataset.cardId)
+    // index < 0 &&
+    if (card.dataset.cardIsread === 'false' && card.dataset.cardId) {
+      notificationMsgStore.concatNotificationMsgList([card.dataset.cardId])
+
+      observer.observe(card)
+    }
+  })
+}
+
+const clearReadData = (data) => {
+  const curData = data || []
+  // 将当前页面剩余未读消息id从监听列表中移除
+  const idsToRemove = curData.map((item: any) => item.info.id)
+  notificationMsgStore.removeNotificationMsgList(idsToRemove)
+
+  // 清除当前页面的所有卡片的监听
+  curPageAllCards.value.forEach((card: any) => {
+    observer.unobserve(card)
+  })
+}
+
+const compareIdsInArrays = (arr1, arr2) => {
+  // 如果两个数组长度不同,则它们不可能有相同的id集合
+  if (arr1.length !== arr2.length) {
+    return false
+  }
+
+  // 提取两个数组中的所有有效id并转换为Set
+  const ids1 = new Set(arr1.filter((item) => item.info && item.info.id).map((item) => item.info.id))
+  const ids2 = new Set(arr2.filter((item) => item.info && item.info.id).map((item) => item.info.id))
+
+  // 比较两个Set的大小是否相同
+  if (ids1.size !== ids2.size) {
+    return false
+  }
+
+  // 检查ids1中的每个id是否都存在于ids2中
+  for (let id of ids1) {
+    if (!ids2.has(id)) {
+      return false
+    }
+  }
+
+  return true
+}
+
+const handleData = (data) => {
+  data.map((item) => {
+    item.id = String(Math.random()).slice(-10)
+  })
+  return data
+}
+const initData = () => {
+  const watchOptions = { deep: true, immediate: true }
+  const handleDataChange = (newData, oldData) => {
+    // 如果id相等,则不需要更新页面数据
+    if (compareIdsInArrays(oldData || [], newData || [])) return
+
+    pageData.value = handleData(cloneDeep(newData))
+
+    // 清除旧数据中的卡片监听
+    clearReadData(oldData)
+
+    // 请求接口将旧数据中的新已读卡片上传服务器
+    if (props.data.updateReadCardsOnChange) {
+      notificationMsgStore.markMessageAsRead()
+    }
+
+    // 重新监听新数据中的卡片
+    nextTick(() => {
+      setTimeout(() => {
+        watchCards()
+      }, 500)
+    })
+  }
+  // 需要监听卡片已读未读状态
+  if (props.isObserver) {
+    watch(
+      () => props.data,
+      (newVal, oldVal) => {
+        handleDataChange(newVal, oldVal)
+      },
+      watchOptions
+    )
+  } else {
+    // 不需要监听
+    watch(
+      () => props.data,
+      (newVal) => {
+        pageData.value = handleData(cloneDeep(newVal))
+      },
+      watchOptions
+    )
+  }
+}
+initData()
+
+// 将数据中的消息置为已读
+const setAllMessageRead = () => {
+  pageData.value.forEach((item) => {
+    item.info.isRead = true
+  })
+}
+
+// 定时将消息卡片未读的置为已读,五分钟一次
+let timer = null
+onMounted(() => {
+  if (props.isObserver) {
+    timer = setInterval(() => {
+      notificationMsgStore.markMessageAsRead()
+    }, 300000)
+  }
+})
+onUnmounted(() => {
+  if (props.isObserver) {
+    notificationMsgStore.markMessageAsRead()
+    clearReadData(pageData.value)
+    clearInterval(timer)
+  }
+})
+
+const handleViewMore = () => {
+  emit('viewMore')
+}
+const parentHeight = computed(() => {
+  return (window.innerHeight || document.documentElement.clientHeight) - props.topOffset
+})
+const scrollParentBoxStyle = computed(() => {
+  return props.topOffset ? { height: `${parentHeight.value}px` } : {}
+})
+
+const scrollContainerRef = ref<HTMLElement | null>(null)
+const loading = ref(false)
+const finished = ref(false)
+const prevScrollTop = ref(0)
+
+const loadMore = async () => {
+  if (loading.value || finished.value) return
+  loading.value = true
+  emit('loading')
+}
+
+const onScroll = () => {
+  const el = scrollContainerRef.value
+  if (!el || loading.value || finished.value) return
+
+  prevScrollTop.value = el.scrollTop + 50
+
+  const threshold = 50 // 提前50px触发
+  if (el.scrollHeight - el.scrollTop - el.clientHeight <= threshold) {
+    loadMore()
+  }
+}
+
+const adjustScrollTop = (scrollTopValue?: number) => {
+  const el = scrollContainerRef.value
+  if (el) {
+    // 如果滚动容器存在,调整其 scrollTop
+    el.scrollTop = scrollTopValue !== undefined ? scrollTopValue : prevScrollTop.value
+  }
+}
+
+defineExpose({
+  loading,
+  finished,
+  scrollContainerRef,
+  adjustScrollTop,
+  setAllMessageRead
+})
+</script>
+
+<template>
+  <div class="scroller" :style="scrollParentBoxStyle" ref="scrollContainerRef" @scroll="onScroll">
+    <template v-for="item in pageData" :key="item.id">
+      <div
+        :class="{ 'scroll-padding': props.isScrollPadding }"
+        :item="item"
+        :size-dependencies="[item.info.isRead]"
+      >
+        <div
+          class="notification-message-card"
+          :data-card-id="item.info.id"
+          :data-card-isread="item.info.isRead"
+        >
+          <EventCard
+            @seeAll="handleSeeAll"
+            @jump-tracking="emit('jumpTracking')"
+            v-if="item.notificationType === 'event'"
+            :data="item.info"
+          />
+          <PasswordCard v-else-if="item.notificationType === 'password'" :data="item.info" />
+          <FeatureUpdateCard
+            @view-more="handleViewMore"
+            v-else-if="item.notificationType === 'feature'"
+            :data="item.info"
+            :is-drawer="props.isDrawer"
+          />
+          <slot></slot>
+          <div class="insertion-time" v-if="isShowInsertionTime">
+            {{ formatTimezone(item.info.first_notifiation_date) }}
+          </div>
+        </div>
+      </div>
+    </template>
+    <div
+      class="footer"
+      v-if="
+        (pageData?.[0]?.notificationType !== 'feature' && pageData?.length > 0) || props.isDrawer
+      "
+    >
+      <el-divider v-if="loading"> loading... </el-divider>
+      <el-divider v-if="finished && pageData.length > 0">
+        Only display the message data within three months
+      </el-divider>
+    </div>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.scroller {
+  width: 100%;
+  overflow-y: auto;
+  .scroll-padding {
+    padding: 0px 140px 0px 16px;
+  }
+  .footer {
+    padding: 0 16px;
+    text-align: center;
+  }
+}
+.notification-message-card {
+  position: relative;
+  .insertion-time {
+    position: absolute;
+    right: 0;
+    top: 4px;
+    font-size: 12px;
+    color: var(--color-neutral-2);
+  }
+}
+</style>

+ 368 - 0
src/components/NotificationMessageCard/src/components/EventCard.vue

@@ -0,0 +1,368 @@
+<script setup lang="ts">
+import { transportationMode } from '@/components/transportationMode'
+import { useRouter } from 'vue-router'
+import { getTimezone } from '@/utils/tools'
+import dayjs from 'dayjs'
+
+const router = useRouter()
+
+interface EventCardPropsData {
+  type: string // 'milestone' | 'container' | 'delay' | 'change'
+  numericRecords?: number // 多条记录数 (Daily Update消息)
+  isRead?: boolean // 是否已读 (true 已读,false 未读)
+  title?: string // Milestone Update
+  mode?: string // 运输方式
+  no: string // HBOL: SHJN2301234
+  tag: string // tag  Booking Confirmed
+  location: string
+  timezone?: string // 时区
+  time: string
+  timeLabel: string
+  serial_no?: string // 单号 用来跳转到Tracking详情页
+  order_from?: string // 订单来源 用来跳转到Tracking详情页
+  insert_date_format?: string // 用来跳转到System Message详情页
+  frequency_type?: string // 用来跳转到System Message详情页
+  rules_type?: string // 用来跳转到System Message详情页
+  is_display_hbol: boolean // 是否显示HBOL
+  previous?: {
+    date: string
+    tag: string
+    time: string
+    timezone: string
+  }
+  info?: {
+    route?: []
+    etdOrdeparturNum?: number
+    etaOrarrivalNum?: number
+    time: string
+    timeLabel: string
+    delayTimeTip: string
+    timezone?: string // 时区
+    leg?: []
+  }
+}
+
+const props = defineProps<{
+  data: EventCardPropsData
+}>()
+
+const emit = defineEmits<{ seeAll: []; jumpTracking: [] }>()
+const handleSeeAll = (data: EventCardPropsData) => {
+  emit('seeAll')
+  router.push({
+    name: 'System Message Detail',
+    query: {
+      frequency_type: data.frequency_type,
+      insert_date_format: data.insert_date_format,
+      rules_type: data.rules_type
+    }
+  })
+}
+
+const jumpTracking = (data: EventCardPropsData) => {
+  emit('jumpTracking')
+  router.push({
+    path: '/tracking/detail',
+    query: { a: data.serial_no, _schemas: data.order_from }
+  })
+}
+</script>
+
+<template>
+  <div class="notification-card" :class="{ 'is-read': data.isRead }">
+    <div class="header" v-if="data.title">
+      <div class="status-icon"></div>
+      <div class="title">{{ data.title }}</div>
+    </div>
+    <div class="content">
+      <div
+        class="more-tips"
+        v-if="(data.type === 'milestone' || data.type === 'container') && data.numericRecords"
+      >
+        <span>Latest Status Updates ({{ data.numericRecords }}) </span>
+        <el-button @click="handleSeeAll(data)" class="see-all-icon el-button--text">
+          See All
+          <span class="font_family icon-icon_next_b"></span>
+        </el-button>
+      </div>
+      <div class="more-tips" v-if="data.info?.etdOrdeparturNum || data.info?.etaOrarrivalNum">
+        <div>
+          <span v-if="data.info?.etdOrdeparturNum"
+            >{{ data.type === 'delay' ? 'Departure Delay' : 'ETD Change' }} ({{
+              data.info?.etdOrdeparturNum
+            }})</span
+          >
+          <span v-if="data.info?.etdOrdeparturNum && data.info?.etaOrarrivalNum">
+            &nbsp;&nbsp;|&nbsp;&nbsp;</span
+          >
+          <span v-if="data.info?.etaOrarrivalNum">
+            {{ data.type === 'delay' ? 'Arrival Delay' : 'ETA Change' }} ({{
+              data.info?.etaOrarrivalNum
+            }})
+          </span>
+        </div>
+        <el-button @click="handleSeeAll(data)" class="see-all-icon el-button--text">
+          See All
+          <span class="font_family icon-icon_next_b"></span>
+        </el-button>
+      </div>
+
+      <div class="base-info">
+        <!-- 除了container类型,其他类型都显示运输方式图标 -->
+        <div style="display: inline-block" v-if="data.type !== 'container'">
+          <span class="font_family" :class="[`icon-${transportationMode?.[data.mode]}`]"></span>
+          <span @click="jumpTracking(data)" class="no no-link"
+            >{{ data.is_display_hbol ? 'HBOL:' : 'MAWB:' }} {{ data.no }}</span
+          >
+        </div>
+        <!-- container类型显示图标 -->
+        <div v-else>
+          <span class="font_family icon-icon_container__filled_b"></span>
+          <span class="no">Container: {{ data.no }}</span>
+        </div>
+        <div class="tag" :class="{ delay: data.type === 'delay', change: data.type === 'change' }">
+          <span class="dot"></span>
+          <span class="text">{{ data.tag }}</span>
+        </div>
+      </div>
+      <div class="route" v-if="data?.info?.route?.length > 0">
+        <span class="font_family icon-icon_route_b"></span>
+        <span>Route:&nbsp;</span>
+        <template v-for="(item, index) in data.info.route" :key="index">
+          <span>{{ item }}</span
+          ><span style="margin: 0 3px" v-if="index !== data.info.route.length - 1">→</span>
+        </template>
+      </div>
+      <!-- change多程情况中的Leg-->
+      <div class="location" v-if="data?.info?.leg?.length > 0">
+        <span class="font_family icon-icon_location_b"></span>
+        <span>Current Leg:&nbsp;</span>
+        <template v-for="(item, index) in data.info.leg" :key="index">
+          <span>{{ item }}</span
+          ><span style="margin: 0 3px" v-if="index !== data.info.leg.length - 1">→</span>
+        </template>
+      </div>
+      <div class="location" v-if="data.location">
+        <span class="font_family icon-icon_location_b"></span>
+        <span>{{ data.location }}</span>
+      </div>
+      <div
+        :class="{ 'delay-time': data.type === 'delay', 'change-time': data.type === 'change' }"
+        v-if="(data.type === 'delay' || data.type === 'change') && data.info?.time"
+      >
+        <span
+          v-if="data.type === 'delay'"
+          style="margin-right: 6px"
+          class="font_family icon-icon_delay_b"
+        ></span>
+        <span v-else class="font_family icon-icon_time_b"></span>
+        <span style="margin-right: 2px" v-if="data.info.timeLabel">{{ data.info.timeLabel }}:</span>
+        <span style="margin-right: 3px">{{
+          dayjs(data.info.time).format('MMM DD, YYYY HH:mm')
+        }}</span>
+        <span>{{ getTimezone(data.info.timezone, data.info.time) }}</span>
+        <span v-if="data.info.delayTimeTip">&nbsp;({{ data.info.delayTimeTip }})</span>
+      </div>
+      <!-- <div class="change-time" v-if="data.type === 'change' && data.info?.time">
+        <span style="margin-left: 1px" class="font_family icon-icon_time_b"></span>
+        <span>{{ data.info?.time }}</span>
+      </div> -->
+      <div class="time" :class="{ grey: data.type === 'delay' || data.type === 'change' }">
+        <span class="font_family icon-icon_time_b"></span>
+        <span style="margin-right: 3px" v-if="data.timeLabel">{{ data.timeLabel }}:</span>
+        <span style="margin-right: 3px">{{ dayjs(data.time).format('MMM DD, YYYY HH:mm') }}</span>
+        <span>{{ getTimezone(data.timezone, data.time) }}</span>
+      </div>
+      <div class="previous" v-if="data.previous">
+        <span class="previous-icon"></span>
+        <span
+          >{{ data.previous.tag }}&nbsp;({{ data.previous.time }}
+          {{ getTimezone(data.previous.timezone, data.previous.time) }})</span
+        >
+      </div>
+    </div>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.notification-card {
+  margin-bottom: 16px;
+  &.is-read {
+    & > .header {
+      .status-icon {
+        background-color: var(--color-border);
+      }
+      .title {
+        color: #b5b9bf;
+      }
+    }
+  }
+  .header {
+    display: flex;
+    align-items: center;
+    margin-bottom: 8px;
+    .status-icon {
+      width: 8px;
+      height: 8px;
+      border-radius: 50%;
+      background-color: var(--color-theme);
+      margin-right: 10px;
+    }
+    .title {
+      flex: 1;
+      font-weight: 700;
+    }
+  }
+  .content {
+    padding: 16px 8px;
+    padding-top: 0;
+    background-color: var(--color-header-bg);
+    border-radius: 6px;
+    .more-tips {
+      display: flex;
+      justify-content: space-between;
+      padding: 8px 0;
+      border-bottom: 1px dashed var(--color-border);
+      font-size: 12px;
+      line-height: 24px;
+      .see-all-icon {
+        width: 68px;
+        height: 24px;
+        font-size: 12px;
+        :deep(span) {
+          color: var(--color-theme);
+        }
+      }
+    }
+    .base-info {
+      display: flex;
+      padding-top: 16px;
+      .no {
+        margin-left: 8px;
+        font-weight: 700;
+        line-height: 18px;
+      }
+      .no-link {
+        &:hover {
+          color: var(--color-theme);
+          cursor: pointer;
+        }
+      }
+      .tag {
+        display: flex;
+        align-items: center;
+        height: 18px;
+        margin-top: 2px;
+        margin-left: 4px;
+        padding-left: 8px;
+        padding-right: 6px;
+        background-color: var(--color-milestone-tag-bg);
+        border-radius: 3px;
+        &.delay {
+          background-color: var(--color-delay-tag-bg);
+          .dot {
+            background-color: #c9353f;
+          }
+          .text {
+            color: #c9353f;
+          }
+        }
+        &.change {
+          background-color: var(--color-change-tag-bg);
+          .dot {
+            background-color: #edb82f;
+          }
+          .text {
+            margin-top: 0;
+            color: #edb82f;
+          }
+        }
+        .dot {
+          width: 5px;
+          height: 5px;
+          border-radius: 50%;
+          background-color: #5bb462;
+          margin-right: 4px;
+        }
+        .text {
+          font-size: 10px;
+          font-weight: 600;
+          color: #5bb462;
+          line-height: 10px;
+          vertical-align: middle;
+        }
+      }
+    }
+    .route,
+    .location,
+    .change-time,
+    .delay-time,
+    .time {
+      display: flex;
+      align-items: center;
+      margin-top: 8px;
+      height: 16px;
+      &.grey {
+        span {
+          color: var(--color-neutral-3);
+        }
+      }
+      span {
+        color: var(--color-neutral-2);
+        font-size: 12px;
+        line-height: 14px;
+      }
+      .font_family {
+        font-size: 16px;
+        font-family: 'iconfont';
+        color: var(--color-neutral-2);
+        margin-right: 8px;
+      }
+    }
+    div.delay-time {
+      height: 19px;
+      margin-top: 6px;
+      margin-bottom: -2px;
+      span,
+      .font_family {
+        color: #c9353f;
+      }
+      span {
+        display: inline-block;
+        margin-top: 3px;
+        line-height: 19px;
+      }
+      .font_family {
+        margin: 0 6px 0 -1px;
+        line-height: 12px;
+        font-size: 18px;
+      }
+    }
+    div.change-time {
+      span,
+      .font_family {
+        color: #edb82f;
+      }
+    }
+    .previous {
+      margin-top: 8px;
+      padding: 4px 8px;
+      line-height: 16px;
+      background-color: var(--color-previous-bg);
+      border-radius: 6px;
+      span {
+        font-size: 12px;
+      }
+      .previous-icon {
+        display: inline-block;
+        width: 4px;
+        height: 4px;
+        background-color: var(--color-neutral-1);
+        border-radius: 50%;
+        margin-right: 8px;
+        vertical-align: middle;
+      }
+    }
+  }
+}
+</style>

+ 164 - 0
src/components/NotificationMessageCard/src/components/FeatureUpdateCard.vue

@@ -0,0 +1,164 @@
+<script setup lang="ts">
+import { useRouter } from 'vue-router'
+
+const router = useRouter()
+
+interface FeatureUpdateCardPropsData {
+  id: string
+  title: string
+  header: string
+  content: string
+  isRead: boolean
+  imgSrc: string
+}
+
+const props = defineProps<{
+  data: FeatureUpdateCardPropsData
+  isDrawer?: boolean // 是否在抽屉中使用
+}>()
+
+const emit = defineEmits<{
+  viewMore: []
+}>()
+const handleViewMore = () => {
+  router.push({
+    name: 'System Message Detail',
+    query: {
+      type: 'feature',
+      title: props.data.header,
+      id: props.data.id
+    }
+  })
+  emit('viewMore')
+}
+
+const handleContent = (header) => {
+  if (header === 'New Feature: AI Smart Assistant') {
+    return 'Smart Assistant is live! Ask in natural language, get real-time shipment data with multi-language support.'
+  }else {
+    return 'Smart Notification is here! Four key event alerts with customizable rules and multi-channel delivery.'
+  }
+}
+</script>
+
+<template>
+  <div>
+    <div class="feature-update" :class="{ 'is-read': data.isRead }">
+      <div class="header">
+        <div class="status-icon"></div>
+        <div class="title">{{ data.title }}</div>
+      </div>
+      <div class="card-content">
+        <div class="title">
+          <!-- <span class="font_family icon-icon_password_b"></span> -->
+          <img style="margin-right: 5px" src="../images/icon_publish.png" alt="" />
+          <span class="gradient-text">{{ data.header }}</span>
+        </div>
+        <div class="content-text">
+          <pre style="white-space: pre-wrap; line-height: 21px" v-if="props.isDrawer">{{ handleContent(data.header) }}</pre>
+          <pre style="white-space: pre-wrap; line-height: 21px" v-else>{{ data.content }}</pre>
+        </div>
+        <div class="feature-img" style="text-align: center">
+          <img
+            :src="data.imgSrc"
+            style="max-height: 400px; max-width: 100%;object-fit: contain"
+            
+            alt="feature-img"
+          ></img>
+          <!-- <img
+            src="./img/testaa.png"
+            alt=""
+            style="max-height: 400px; max-width: 100%; object-fit: contain"
+          /> -->
+        </div>
+
+        <div class="change-btn" style="text-align: center">
+          <!-- <el-button
+            @click="handleViewMore"
+            class="el-button--main"
+            style="height: 40px; padding: 0 32px"
+          >
+            View more</el-button
+          > -->
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.feature-update {
+  margin-bottom: 16px;
+  &.is-read {
+    & > .header {
+      .status-icon {
+        background-color: var(--color-border);
+      }
+      .title {
+        color: #b5b9bf;
+      }
+    }
+  }
+  .header {
+    display: flex;
+    align-items: center;
+    margin-bottom: 8px;
+    .status-icon {
+      width: 8px;
+      height: 8px;
+      border-radius: 50%;
+      background-color: var(--color-theme);
+      margin-right: 10px;
+    }
+    .title {
+      font-weight: 700;
+    }
+  }
+  .card-content {
+    padding: 16px 16px 24px 8px;
+    background: linear-gradient(
+      137deg,
+      var(--color-feature-card-first-bg) 12.41%,
+      var(--color-feature-card-second-bg) 52.63%,
+      var(--color-feature-card-third-bg) 93.28%
+    );
+    border-radius: 12px;
+    .title {
+      img {
+        vertical-align: middle;
+      }
+      span {
+        vertical-align: middle;
+        display: inline-block;
+        font-weight: 700;
+      }
+      .gradient-text {
+        background: linear-gradient(90deg, #dc6c6d 0%, #7959c8 46%, #ed6d00 100%);
+        background-clip: text;
+        -webkit-background-clip: text;
+        color: transparent;
+        font-size: 14px;
+        font-weight: bold;
+      }
+      .font_family {
+        margin-right: 8px;
+      }
+    }
+    .content-text {
+      margin: 8px 0 0px 24px;
+    }
+    .feature-img {
+      margin-top: 12px;
+      margin-bottom: 16px;
+      padding-left: 24px;
+      .el-image {
+        display: block;
+        margin: 0 auto;
+        width: 352px;
+        height: 200px;
+        border-radius: 8px;
+      }
+    }
+  }
+}
+</style>

+ 105 - 0
src/components/NotificationMessageCard/src/components/PasswordCard.vue

@@ -0,0 +1,105 @@
+<script setup lang="ts">
+import ChangePasswordDialog from '@/views/Layout/src/components/Header/components/ChangePasswordDialog.vue'
+
+interface PasswordCardPropsData {
+  title: string
+  isExpiration: boolean // true为红色,false为绿色
+  header: string
+  isRead: boolean
+  content: string
+}
+const props = defineProps<{
+  data: PasswordCardPropsData
+}>()
+
+const changePasswordDialogRef = ref()
+const handleChangePassword = () => {
+  changePasswordDialogRef.value.openDialog()
+}
+</script>
+
+<template>
+  <div class="password-notifications" :class="{ 'is-read': data.isRead }">
+    <div class="header" v-if="data.title">
+      <div class="status-icon"></div>
+      <div class="title">{{ data.title }}</div>
+    </div>
+    <div class="card-content" :class="{ 'is-expired': data.isExpiration }">
+      <div class="title">
+        <span class="font_family icon-icon_password_b"></span>
+        <span>{{ data.header }}</span>
+      </div>
+      <div class="details">
+        {{ data.content }}
+      </div>
+      <div class="change-btn" style="text-align: center">
+        <el-button @click="handleChangePassword" class="el-button--main" style="height: 40px">
+          <span class="font_family icon-icon_edit_b" style="margin-right: 4px"></span>
+          <span>Change Password</span></el-button
+        >
+      </div>
+    </div>
+    <ChangePasswordDialog ref="changePasswordDialogRef"></ChangePasswordDialog>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.password-notifications {
+  margin-bottom: 16px;
+  &.is-read {
+    & > .header {
+      .status-icon {
+        background-color: var(--color-border);
+      }
+      .title {
+        color: #b5b9bf;
+      }
+    }
+  }
+  .header {
+    display: flex;
+    align-items: center;
+    margin-bottom: 8px;
+    .status-icon {
+      width: 8px;
+      height: 8px;
+      border-radius: 50%;
+      background-color: var(--color-theme);
+      margin-right: 10px;
+    }
+    .title {
+      font-weight: 700;
+    }
+  }
+  .card-content {
+    padding: 16px 8px 24px;
+    background: linear-gradient(
+      180deg,
+      var(--color-password-card-first-bg) 0%,
+      var(--color-password-card-second-bg) 100%
+    );
+    border-radius: 12px;
+    &.is-expired {
+      background: linear-gradient(
+        182deg,
+        var(--color-password-expired-card-first-bg) 2.2%,
+        var(--color-password-expired-card-second-bg) 98.77%
+      );
+    }
+    .title {
+      span {
+        vertical-align: middle;
+        font-weight: 700;
+        color: #2b2f36;
+      }
+      .font_family {
+        margin-right: 8px;
+      }
+    }
+    .details {
+      margin: 8px 0 16px 24px;
+      color: #2b2f36;
+    }
+  }
+}
+</style>

二進制
src/components/NotificationMessageCard/src/images/icon_publish.png


+ 37 - 12
src/components/ScoringGrade/src/ScoringGrade.vue

@@ -14,9 +14,14 @@ import happyPng from '../image/score_happy.png'
 import happyPng2 from '../image/happy_2.png'
 import normalPng from '../image/score_normal.png'
 import submitsucessful from '../image/submit_successful.png'
+import { useUserStore } from '@/stores/modules/user'
+import emitter from '@/utils/bus'
+
+const userStore = useUserStore()
 
 // const isShow = ref(true)
 const visible = ref(false)
+const isShowScoring = ref(true)
 const isLoaded = ref(false)
 const isShowAngry = ref(false)
 const isShowHappy = ref(false)
@@ -202,7 +207,7 @@ const changeSmileRadio = (val: any) => {
 }
 // 提交details
 const submitDetails = (val: any) => {
-  const username = localStorage.getItem('account') ? localStorage.getItem('account') : ''
+  const username = userStore.userName
   if (angryCheckbox.value.length) {
     $api
       .scoringgrade({
@@ -270,9 +275,22 @@ const mouseout = (item: any) => {
   item.src = item.src1
 }
 const SubmitText = ref()
+
+// 切换隐藏icon
+const Logout = () => {
+  isShowScoring.value = false
+}
+// 切换隐藏icon
+const checknoPrompt = () => {
+  isShowScoring.value = true
+}
+onMounted(() => {
+  emitter.on('checkPrompt', Logout)
+  emitter.on('checknoPrompt', checknoPrompt)
+})
 </script>
 <template>
-  <div class="scoring">
+  <div class="scoring" v-if="isShowScoring">
     <el-popover
       :visible="visible"
       placement="left"
@@ -281,7 +299,7 @@ const SubmitText = ref()
       popper-class="popver_class"
     >
       <template #reference>
-        <el-avatar @click="avatarClick" :size="46" shape="square" :src="clickSrc" />
+        <el-avatar class="avatar_bg" @click="avatarClick" :size="46" shape="square" :src="clickSrc" />
       </template>
       <div class="score_flex">
         <el-popover
@@ -387,19 +405,26 @@ const SubmitText = ref()
 </template>
 
 <style lang="scss">
-.scoring {
-  width: 64px;
-  height: 64px;
-  border-radius: 12px 0 0 12px;
+div.scoring {
+  width: 50px;
+  height: 50px;
+  border-radius: 12px;
   background-color: var(--management-bg-color);
   position: absolute;
   box-shadow: -2px 2px 12px rgba(0, 0, 0, 15%);
-  z-index: 2013;
-  right: 0;
-  bottom: 80px;
+  z-index: 1999;
+  right: 10px;
+  bottom: 64px;
   display: flex;
   align-items: center;
   justify-content: center;
+  .avatar_bg {
+    img {
+      width: 36px;
+      height: 36px;
+      margin: 3px 0 0 4px;
+    }
+  }
 }
 .el-avatar {
   background-color: var(--management-bg-color);
@@ -410,7 +435,7 @@ const SubmitText = ref()
   height: 64px;
   align-items: center;
   position: absolute;
-  right: 64px;
+  right: 84px;
   top: -30px;
   background-color: var(--management-bg-color);
   border-radius: 12px 0 0 12px;
@@ -419,7 +444,7 @@ const SubmitText = ref()
   margin: 0 16px;
 }
 .el-popover.popver_class {
-  border-radius: 12px 0 0 12px !important;
+  border-radius: 12px !important;
   box-shadow: none;
   filter: drop-shadow(-2px 2px 12px rgba(0, 0, 0, 0.15));
   right: 0 !important;

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

@@ -2,6 +2,7 @@
 import _ from 'lodash'
 import { reactive, ref, onMounted, watch } from 'vue'
 import { ElMessage } from 'element-plus'
+import { formatNumber } from '@/utils/tools'
 
 const emit = defineEmits(['check', 'input'])
 const props = defineProps({
@@ -244,7 +245,7 @@ const onInput = () => {
         <el-table-column property="uncode" label="Uncode" />
       </el-table>
       <div class="pagination">
-        <span>Total {{ state.total }}</span>
+        <span>Total {{ formatNumber(state.total) }}</span>
         <el-pagination
           v-model:currentPage="state.currentPage"
           v-model:page-size="state.pageSize"

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

@@ -75,14 +75,16 @@ const changeSelect = (val: any) => {
 const emit = defineEmits(['changeAutoSelectAddType', 'delSelect', 'changeAutoSelect'])
 const errorBoolean = ref(false)
 let AutoSelectObj: any = {}
+let AutoSelectObj2: any = {}
 const changeAutoSelect = (val: any, value: any) => {
   if (value.length) {
     errorBoolean.value = true
   } else {
     errorBoolean.value = false
   }
-  AutoSelectObj[val] = value
-  emit('changeAutoSelect', AutoSelectObj, errorBoolean.value)
+  AutoSelectObj[val] = value.join()
+  AutoSelectObj2[val] = value
+  emit('changeAutoSelect', AutoSelectObj, AutoSelectObj2,errorBoolean.value)
 }
 const typeSelectFocus = (index: any, e: any) => {
   typeSelectIndex.value = index

+ 2 - 6
src/components/ShipmentStatus/src/ShipmentStatus.vue

@@ -1,6 +1,6 @@
 <script setup lang="ts">
-import dayjs from 'dayjs'
 import { useOverflow } from '@/hooks/useOverflow'
+import { formatTimezone } from '@/utils/tools'
 
 const props = defineProps({
   data: Object
@@ -34,10 +34,6 @@ const getDateHeight = (index: number) => {
   return 42 + 26 * index
 }
 
-const formatDate = (date: string) => {
-  return date ? dayjs(date).format('MMM-DD-YYYY') : '--'
-}
-
 const pathRef = ref()
 
 const { isOverflow: isPathOverflow } = useOverflow(pathRef, props)
@@ -76,7 +72,7 @@ const { isOverflow: isPathOverflow } = useOverflow(pathRef, props)
             <div class="step-dot"></div>
             <div class="label">{{ dateItem.label }}</div>
             <div class="divider"></div>
-            <div class="date">{{ formatDate(dateItem.date) }}</div>
+            <div class="date">{{ formatTimezone(dateItem.date) }}</div>
           </div>
         </div>
       </div>

+ 46 - 0
src/components/TableEmpty/TableEmpty.vue

@@ -0,0 +1,46 @@
+<script setup lang="ts">
+import { computed } from 'vue'
+import lightPng from './image/default_notification_setting@2x.png'
+import darkPng from './image/default_notification_setting@2x.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="v-empty">
+    <div class="empty-img">
+      <img :src="emptyImg" alt="" style="width: 100px;" />
+    </div>
+    <p class="title">
+      <slot name="title">{{ props.EmptyTitle }}</slot>
+    </p>
+    <div>
+      <slot name="suggestion"></slot>
+    </div>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.v-empty {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  .empty-img {
+    margin-bottom: 16px;
+  }
+  .title {
+    margin-bottom: 8px;
+    font-weight: 400;
+    color: var(--color-neutral-2);
+  }
+}
+</style>

二進制
src/components/TableEmpty/image/default_notification_setting.png


二進制
src/components/TableEmpty/image/default_notification_setting@2x.png


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

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

+ 22 - 18
src/components/TransportMode/src/TransportMode.vue

@@ -141,15 +141,17 @@ const defaultTransport = () => {
       } else {
         searchTableQeuryTracking.value =
           JSON.parse(sessionStorage.getItem('searchTableQeuryTracking') as string) || {}
-        if (searchTableQeuryTracking.value.transport_mode.length !== 0) {
-          TransportData.data = searchTableQeuryTracking.value.transport_mode
-          searchTableQeuryTracking.value.transport_mode.forEach((item: any) => {
-            if (item == 'All') {
-              checkAll.value = true
-            } else {
-              checkAll.value = false
-            }
-          })
+        if (searchTableQeuryTracking.value.transport_mode != undefined) {
+          if (searchTableQeuryTracking.value.transport_mode.length !== 0) {
+            TransportData.data = searchTableQeuryTracking.value.transport_mode
+            searchTableQeuryTracking.value.transport_mode.forEach((item: any) => {
+              if (item == 'All') {
+                checkAll.value = true
+              } else {
+                checkAll.value = false
+              }
+            })
+          }
         } else {
           TransportData.data = []
           checkAll.value = false
@@ -180,15 +182,17 @@ const defaultTransport = () => {
     } else {
       searchTableQeury.value =
         JSON.parse(sessionStorage.getItem('searchTableQeury') as string) || {}
-      if (searchTableQeury.value.transport_mode.length !== 0) {
-        TransportData.data = searchTableQeury.value.transport_mode
-        searchTableQeury.value.transport_mode.forEach((item: any) => {
-          if (item == 'All') {
-            checkAll.value = true
-          } else {
-            checkAll.value = false
-          }
-        })
+      if (searchTableQeury.value.transport_mode !== undefined) {
+        if (searchTableQeury.value.transport_mode.length !== 0) {
+          TransportData.data = searchTableQeury.value.transport_mode
+          searchTableQeury.value.transport_mode.forEach((item: any) => {
+            if (item == 'All') {
+              checkAll.value = true
+            } else {
+              checkAll.value = false
+            }
+          })
+        }
       } else {
         TransportData.data = []
         checkAll.value = false

+ 61 - 5
src/components/VBreadcrumb/src/VBreadcrumb.vue

@@ -6,6 +6,7 @@ import { useThemeStore } from '@/stores/modules/theme'
 const router = useRouter()
 const breadCrumb = useBreadCrumb()
 const themeStore = useThemeStore()
+const CancelRulesVisible = ref(false)
 
 const handleGoBack = () => {
   const routeData = breadCrumb.getUpperRoute()
@@ -14,12 +15,25 @@ const handleGoBack = () => {
     query: routeData.query
   })
 }
+let monitoringQuery = ref()
 const jumpLink = (label: string, query: any) => {
-  label &&
-    router.push({
-      name: label,
-      query: query
-    })
+    if(label == 'Monitoring Settings') {
+      CancelRulesVisible.value = true
+      monitoringQuery.value = query
+    } else {
+      label &&
+      router.push({
+        name: label,
+        query: query
+      })
+    }
+}
+const jumpLinkMonitoring = () => {
+  CancelRulesVisible.value = false
+  router.push({
+    name: 'Monitoring Settings',
+    query: monitoringQuery.value
+  })
 }
 </script>
 
@@ -41,6 +55,31 @@ const jumpLink = (label: string, query: any) => {
     </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>
 </template>
 
 <style lang="scss" scoped>
@@ -72,4 +111,21 @@ const jumpLink = (label: string, query: any) => {
     }
   }
 }
+.cancel_header {
+  font-size: 18px;
+  font-weight: 700;
+  color: var(--color-neutral-1);
+  display: flex;
+  align-items: center;
+}
+.icon_warning {
+  width: 22px;
+  height: 22px;
+  margin-right: 0;
+  fill: var(--color-btn-warning-bg);
+}
+.iconfont_warning {
+  display: flex;
+  align-items: center;
+}
 </style>

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

@@ -36,7 +36,7 @@ const props = withDefaults(defineProps<internalProps>(), {
 <style scoped>
 .v-loading-mask {
   position: absolute;
-  z-index: 2000;
+  z-index: 1499;
   margin: 0;
   top: 0;
   right: 0;

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

@@ -173,9 +173,6 @@ defineExpose({
   width: 400px;
   height: 365px;
   padding: 40px;
-  .tips {
-    text-align: center;
-  }
 }
 </style>
 <style lang="scss">

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

@@ -24,6 +24,7 @@ interface Props {
 interface optionsItem {
   value: string
   label: string
+  checked: boolean
 }
 
 const list = ref<ListItem[]>([])
@@ -67,7 +68,7 @@ watch(
     deep: true
   }
 )
-const ErrorNumber = ref(true)
+const ErrorNumber = ref(false)
 const str = ref()
 const search_field = ref()
 const InputAutoSelect = (val: any) => {
@@ -94,7 +95,7 @@ const remoteMethod = (query: string) => {
           if (res.code == 200) {
             loading.value = false
             list.value = res.data.map((item: any) => {
-              return { value: item, label: item }
+              return { value: item, label: item, checked: testAuto.value?.includes(item) }
             })
             options.value = list.value.filter((item) => {
               return item.label.toLowerCase().includes(query.toLowerCase())
@@ -119,7 +120,9 @@ const changeSelect = (val: any) => {
 const emit = defineEmits(['changeAutoSelectAddType', 'delSelect', 'changeAutoSelect'])
 let AutoSelectObj: any = {}
 let AutoSelectObj2: any = {}
+const testAuto = ref()
 const changeAutoSelect = (val: any, value: any) => {
+  testAuto.value = value
   AutoSelectObj[val] = value.join()
   AutoSelectObj2[val] = value
   if (value.length) {
@@ -235,8 +238,9 @@ const typeSelectClick = (index: any, val: any) => {
           :label="item.label"
           :value="item.value"
         >
-          <el-checkbox :checked="AddType[index].partyname?.includes(item.value)"></el-checkbox>
-          <div class="label">{{ item.value }}</div>
+          <el-checkbox :checked="item.checked">
+            <span class="label" @click="item.checked = !item.checked">{{ item.value }}</span>
+          </el-checkbox>
         </el-option>
       </el-select>
       <div

+ 75 - 3
src/router/index.ts

@@ -42,6 +42,14 @@ const router = createRouter({
             activeMenu: '/tracking'
           }
         },
+        {
+          path: '/shipment/detail',
+          name: 'Shipment Detail',
+          component: () => import('../views/Tracking/src/components/TrackingDetail'),
+          meta: {
+            activeMenu: '/tracking'
+          }
+        },
         {
           path: '/tracking/add-vgm',
           name: 'Add VGM',
@@ -87,6 +95,53 @@ const router = createRouter({
           path: '/Operationlog',
           name: 'Operationlog',
           component: () => import('../views/OperationLog')
+        },
+        {
+          path: '/chat-log',
+          name: 'Chat Log',
+          component: () => import('../views/ChatLog')
+        },
+        {
+          path: '/ai-api-log',
+          name: 'AI API Log',
+          component: () => import('../views/AIApiLog')
+        },
+        {
+          path: '/PromptConfiguration',
+          name: 'PromptConfiguration',
+          component: () => import('../views/PromptConfiguration')
+        },
+        {
+          path: '/system-message',
+          name: 'System Message',
+          component: () => import('../views/SystemMessage')
+        },
+        {
+          path: '/system-message-detail',
+          name: 'System Message Detail',
+          meta: {
+            breadName: 'Detail',
+            activeMenu: '/system-message'
+          },
+          component: () => import('../views/SystemMessage/src/components/SystemMessageDetail.vue')
+        },
+        {
+          path: '/SystemSettings',
+          name: 'Monitoring Settings',
+          component: () => import('../views/SystemSettings')
+        },
+        {
+          path: '/SystemSettings',
+          name: 'System Settings',
+          component: () => import('../views/SystemSettings')
+        },
+        {
+          path: '/SystemSettings/createnewrule',
+          name: 'Create New Rule',
+          component: () => import('../views/SystemSettings/src/components/CreateNewrule'),
+          meta: {
+            activeMenu: '/SystemSettings'
+          }
         }
       ]
     }
@@ -96,20 +151,37 @@ const router = createRouter({
 // * 路由拦截 beforeEach
 router.beforeEach(async (to, from, next) => {
   useBreadCrumb().setRouteList(to)
+  const userStore = useUserStore()
   // 如果手动跳转登录页,清除登录信息
   if (to.path === '/login') {
-    if (localStorage.getItem('username')) {
-      const userStore = useUserStore()
+    if (userStore.isLogin) {
       await userStore.logout()
     }
     sessionStorage.removeItem('trackingTablePageInfo')
     sessionStorage.removeItem('bookingTablePageInfo')
   }
 
+  // 判断是否从systemMessage详情页跳转到systemMessage列表页,或者从systemMessage列表页跳转到systemMessage详情页
+  if (
+    !(
+      from.name === 'System Message Detail' ||
+      from.name === 'System Message' ||
+      !from.name ||
+      from.name === 'Tracking Detail'
+    ) ||
+    !(
+      to.name === 'System Message' ||
+      to.name === 'System Message Detail' ||
+      to.name === 'Tracking Detail'
+    )
+  ) {
+    sessionStorage.removeItem('activeCardTypeName')
+  }
+
   // 未登录白名单
   const whiteList = ['/login', '/public-tracking', '/public-tracking/detail', '/reset-password']
   // 判断是否登录
-  if (!whiteList.includes(to.path) && !localStorage.getItem('username')) {
+  if (!whiteList.includes(to.path) && !userStore.isLogin) {
     const userStore = useUserStore()
     await userStore.logout()
     if (whiteList.includes(from.path)) {

+ 36 - 2
src/stores/modules/breadCrumb.ts

@@ -1,4 +1,5 @@
 import { defineStore } from 'pinia'
+import { collapseTextChangeRangesAcrossMultipleVersions } from 'typescript'
 
 interface Route {
   label: string
@@ -9,7 +10,14 @@ interface BreadCrumb {
   routeList: Route[]
 }
 // 需要添加多级菜单的页面,值为route的name
-const whiteList = ['Booking Detail', 'Tracking Detail', 'Add VGM', 'Public Tracking Detail']
+const whiteList = [
+  'Booking Detail',
+  'Tracking Detail',
+  'Add VGM',
+  'Public Tracking Detail',
+  'Create New Rule',
+  'System Message Detail'
+]
 
 export const useBreadCrumb = defineStore('breadCrumb', {
   state: (): BreadCrumb => ({
@@ -34,9 +42,35 @@ export const useBreadCrumb = defineStore('breadCrumb', {
             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 && whiteList.includes(toRoute.name)) {
         this.routeList.push({
-          label: toRoute.name,
+          label: toRoute?.meta?.breadName || toRoute.name,
           path: toRoute.path,
           query: toRoute.query
         })

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

@@ -0,0 +1,71 @@
+import { defineStore } from 'pinia'
+
+interface NotificationMessageState {
+  notificationMsgList: string[] // 页面中监听的未读消息id
+  readCardMap: string[] // 已读的消息id
+  hasNewMsg: boolean
+}
+
+export const useNotificationMessage = defineStore('notificationMessage', {
+  state: (): NotificationMessageState => ({
+    notificationMsgList: JSON.parse(localStorage.getItem('notificationMsgList')) || [],
+    readCardMap: JSON.parse(localStorage.getItem('readCardMap')) || [],
+    hasNewMsg: JSON.parse(localStorage.getItem('hasNewMsg')) || false
+  }),
+  getters: {},
+  actions: {
+    concatNotificationMsgList(array: any[]) {
+      this.notificationMsgList = [...new Set([...this.notificationMsgList, ...array])]
+      localStorage.setItem('notificationMsgList', JSON.stringify(this.notificationMsgList))
+    },
+    removeNotificationMsgList(array: any[]) {
+      this.notificationMsgList = this.notificationMsgList.filter((item) => !array.includes(item))
+
+      localStorage.setItem('notificationMsgList', JSON.stringify(this.notificationMsgList))
+    },
+    setReadCardMap(id: string) {
+      // 将页面中从未读到已读的消息id存入readCardMap
+      if (this.readCardMap.includes(id)) return
+      this.readCardMap.push(id)
+      localStorage.setItem('readCardMap', JSON.stringify(this.readCardMap))
+      // 将已读的消息从notificationMsgList中删除
+      this.notificationMsgList = this.notificationMsgList.filter((item) => {
+        return !this.readCardMap.includes(item)
+      })
+      localStorage.setItem('notificationMsgList', JSON.stringify(this.notificationMsgList))
+    },
+    hasUnreadMessages() {
+      $api.hasUnreadMessages().then((res) => {
+        if (res.code === 200) {
+          this.hasNewMsg = res.data.has_message
+          localStorage.setItem('hasNewMsg', JSON.stringify(this.hasNewMsg))
+        }
+      })
+    },
+    setHasNewMsg() {
+      this.hasNewMsg = true
+      localStorage.setItem('hasNewMsg', JSON.stringify(this.hasNewMsg))
+    },
+    async markMessageAsRead() {
+      if (this.readCardMap.length === 0) return
+
+      await $api.setMessageRead({ id: this.readCardMap }).then((res) => {
+        if (res.code === 200) {
+          this.readCardMap = []
+          localStorage.setItem('readCardMap', JSON.stringify(this.readCardMap))
+
+          // 在将消息标记为已读后,再次检查是否有新消息
+          this.hasUnreadMessages()
+        }
+      })
+    },
+    clearData() {
+      this.notificationMsgList = []
+      this.readCardMap = []
+      this.hasNewMsg = false
+      localStorage.removeItem('hasNewMsg')
+      localStorage.removeItem('notificationMsgList')
+      localStorage.removeItem('readCardMap')
+    }
+  }
+})

+ 71 - 8
src/stores/modules/user.ts

@@ -1,38 +1,101 @@
 import { defineStore } from 'pinia'
 import { useVisitedRowState } from './visitedRow'
+import { useNotificationMessage } from './notificationMessage'
+import dayjs from 'dayjs'
 
+interface UserInfo {
+  uname: string
+  first_name: string
+  last_name: string
+  user_type: string
+  email: string
+  expire_day: number
+  date_format: string
+  numbers_format: string
+  PASSWORD_CHANGE_CYCLE: number // 密码修改周期(多少天需要改一次)
+  last_pwd_change: string // 上次密码修改时间
+}
 interface UserState {
-  username: string
+  userInfo: UserInfo
   isFirstLogin: boolean
 }
+
+/**
+ * 根据用户地区判断日期格式
+ * @returns {string} - 返回日期格式
+ */
+const getDateFormat = () => {
+  const userLanguage = navigator.language || 'en-US' // 获取浏览器的语言设置
+  // 判断用户地区
+  if (userLanguage === 'en-US') {
+    return 'MM/DD/YYYY' // 美国使用 MM/DD/YYYY 格式
+  } else if (
+    userLanguage.startsWith('de') ||
+    userLanguage.startsWith('fr') ||
+    userLanguage.startsWith('it') ||
+    userLanguage.startsWith('es') ||
+    userLanguage.startsWith('pl') ||
+    userLanguage.startsWith('nl') ||
+    userLanguage.startsWith('pt') ||
+    userLanguage.startsWith('se')
+  ) {
+    return 'DD/MM/YYYY' // 其他欧洲国家(例:德语、法语、西班牙语等)使用 DD/MM/YYYY 格式
+  } else if (['zh-CN', 'ja-JP', 'ko-KR'].includes(userLanguage)) {
+    return 'YYYY-MM-DD' // 东亚国家(如简体中文、日语、韩语)使用 YYYY-MM-DD 格式
+  } else {
+    return 'DD/MM/YYYY' // 其他地区默认 DD/MM/YYYY 格式
+  }
+}
+
 export const useUserStore = defineStore('user', {
   state: (): UserState => ({
-    username: localStorage.getItem('username') || '',
+    userInfo: JSON.parse(localStorage.getItem('userInfo') || '{}'),
     isFirstLogin: localStorage.getItem('isFirstLogin')
       ? JSON.parse(localStorage.getItem('isFirstLogin'))
       : false
   }),
-  getters: {},
+  getters: {
+    userName(state) {
+      if (state.userInfo.first_name && state.userInfo.last_name) {
+        return `${state.userInfo.first_name} ${state.userInfo.last_name}`
+      } else {
+        return state.userInfo.uname || ''
+      }
+    },
+    dateFormat(state) {
+      return state.userInfo.date_format || getDateFormat()
+    },
+    expireDay(state) {
+      const userInfo = state.userInfo
+      return userInfo.PASSWORD_CHANGE_CYCLE - dayjs().diff(dayjs(userInfo.last_pwd_change), 'day')
+    },
+    isLogin(state) {
+      return !(!state.userInfo?.uname || (state.userInfo?.uname && state.isFirstLogin === true))
+    }
+  },
   actions: {
-    setUsername(username: any, isFirstLogin?: boolean) {
-      localStorage.setItem('username', username)
-      this.username = username
+    setUserInfo(userInfo: any, isFirstLogin?: boolean) {
+      this.userInfo = userInfo
+      localStorage.setItem('userInfo', JSON.stringify(userInfo))
+
       if (isFirstLogin !== undefined) {
         localStorage.setItem('isFirstLogin', JSON.stringify(isFirstLogin))
         this.isFirstLogin = isFirstLogin
       }
     },
+
     async logout(isNeedLogout: boolean = true) {
       if (isNeedLogout) {
         await $api.logout().then(() => {})
       }
-      localStorage.removeItem('username')
-      this.username = ''
+      localStorage.removeItem('userInfo')
+      this.userInfo = {}
       if (localStorage.getItem('isFirstLogin')) {
         localStorage.removeItem('isFirstLogin')
       }
       this.isFirstLogin = false
       useVisitedRowState().clearVisitedRow()
+      useNotificationMessage().clearData()
     }
   }
 })

+ 103 - 23
src/styles/elementui.scss

@@ -15,6 +15,21 @@
   }
 }
 
+.el-button.el-button--noborder--configuration {
+  border: none;
+  span {
+    color: var(--color-theme);
+  }
+  &:hover {
+    border-color: var(--color-btn-default-bg-hover);
+    background-color: var(--color-btn-default-bg-hover);
+    fill: var(--color-theme);
+    span {
+      color: var(--color-theme);
+    }
+  }
+}
+
 button.el-button.el-button--text {
   height: 24px;
   padding: 4px 8px;
@@ -49,7 +64,7 @@ button.el-button.el-button--text {
 
 .el-button--main.is-plain {
   background-color: var(--color-white);
-  border: 1px solid var(--color-border);
+  border: 1px solid var(--color-theme);
   span {
     color: var(--color-theme);
   }
@@ -75,19 +90,15 @@ button.el-button--main {
     fill: var(--color-white);
   }
 }
-
-button.el-button.el-button--pain-theme {
-  border: 1px solid var(--color-el-btn-pain-theme-border);
-  background-color: var(--color-el-btn-pain-theme-bg);
-
-  fill: var(--color-el-btn-pain-theme-text);
-  color: var(--color-el-btn-pain-theme-text);
+button.el-button--main.is-disabled {
+  border: none;
+  background-color: var(--color-btn-main-bg-disabled);
   span {
-    color: var(--color-el-btn-pain-theme-text);
+    color: var(--color-white);
   }
+  fill: var(--color-white);
   &:hover {
-    border-color: var(--color-el-btn-pain-theme-border);
-    background-color: var(--color-el-btn-pain-theme-bg-hover);
+    background-color: var(--color-btn-main-bg-disabled);
     color: var(--color-white);
     fill: var(--color-white);
   }
@@ -175,7 +186,7 @@ button.el-button.el-button--icon {
   }
 }
 // 初始为黑色
-.el-button.el-button--dark {
+button.el-button.el-button--dark {
   background-color: var(--color-btn-default-dark-bg);
   fill: var(--color-white);
   border: none;
@@ -187,7 +198,17 @@ button.el-button.el-button--icon {
     background-color: var(--color-btn-default-dark-hover-bg);
     fill: var(--color-btn-default-dark-hover-bg);
     span {
-      color: var(--color-btn-default-dark-hover) !important;
+      color: var(--color-btn-default-dark-hover);
+    }
+  }
+}
+button.el-button.el-button--dark.is-disabled {
+  opacity: 0.3;
+  &:hover {
+    background-color: var(--color-btn-default-dark-bg);
+    fill: var(--color-white);
+    span {
+      color: var(--color-white);
     }
   }
 }
@@ -261,6 +282,7 @@ label.el-radio {
     background-color: var(--color-theme);
     border-color: var(--color-theme);
   }
+
   .el-radio__inner {
     height: 16px;
     width: 16px;
@@ -341,7 +363,7 @@ div.el-drawer {
     height: 64px;
     padding: 16px;
     margin-bottom: 0;
-    background-color: var(--color-table-header-bg);
+    background-color: var(--color-header-bg);
     & > span {
       font-weight: 700;
       font-size: 24px;
@@ -403,11 +425,10 @@ div .el-dropdown__popper .el-dropdown__list {
   user-select: none;
 }
 div .el-checkbox__inner:hover {
-  border-color: var(--color-select-border);
+  border-color: var(--color-system-checkbox-border);
 }
 div .el-checkbox__inner {
-  border-color: var(--color-select-border);
-  background-color: #fff;
+  border: 1px solid var(--color-system-checkbox-border);
 }
 div .el-checkbox__input.is-checked .el-checkbox__inner:after {
   border-color: var(--color-mode);
@@ -443,11 +464,11 @@ html.dark .el-checkbox.el-checkbox--large span.el-checkbox__inner::after {
   width: 5px; /* 打勾图标宽度 */
   height: 10px; /* 打勾图标高度 */
 }
-div .el-popper__arrow,
-div .el-popper__arrow:before {
-  // height: 0;
-  // width: 0;
-}
+// div .el-popper__arrow,
+// div .el-popper__arrow:before {
+// height: 0;
+// width: 0;
+// }
 .el-popper.is-dark,
 div.el-popper.is-dark > .el-popper__arrow:before {
   background-color: var(--color-el-popper-bg);
@@ -518,7 +539,6 @@ div .el-select-dropdown__item.is-hovering {
 .el-select-dropdown__item {
   border-radius: var(--border-radius-6);
   margin: 0 8px;
-
   margin-bottom: 4px;
   &:last-child {
     margin-bottom: 0;
@@ -787,3 +807,63 @@ div .DaterangeClass {
   border-color: var(--management-bg-color) !important;
   border-radius: 12px !important;
 }
+div .el-radio__label {
+  width: 100%;
+}
+
+div .avatar_bg {
+  background-color: transparent !important;
+}
+div .carousel .el-carousel__indicator--horizontal {
+  padding: 4px;
+}
+div .carousel .el-carousel__button {
+  width: 6px;
+  height: 6px;
+  border-radius: 50%;
+  background-color: var(--color-system-card-bg);
+  opacity: 1;
+}
+div .carousel .el-carousel__indicator.is-active button {
+  background-color: var(--color-theme);
+}
+div .carousel .el-carousel__item--card, .el-carousel__item.is-animating {
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  padding-bottom: 9px;
+}
+div .carousel .el-carousel__arrow {
+  opacity: 1;
+  background-color: var(--color-carousel-card-bg);
+  box-shadow: 2px 2px 12px 0px rgba(0, 0, 0, 0.10);
+}
+div .carousel .el-carousel__arrow--left {
+  left: 0;
+}
+div .carousel .el-carousel__arrow--right {
+  right: 0;
+}
+div .carousel .el-icon {
+  fill: black;
+  color: black;
+}
+div .carousel .el-carousel__arrow:hover {
+  background-color: var(--color-arrow-hoverL);
+  .el-icon {
+    fill: var(--color-theme);
+    color: var(--color-theme);
+  }
+}
+div .prompt-dialog {
+  min-height: 800px ;
+}
+div .prompt-dialog-inner .el-dialog__header {
+  padding: 0;
+  height: 48px;
+}
+div .prompt-dialog-inner .el-dialog__body {
+  max-height: 720px;
+  overflow-y: scroll;
+  line-height: 21px; 
+}

+ 6 - 0
src/styles/index.scss

@@ -24,6 +24,12 @@
   font-family: 'Lato-Light', 'Microsoft JhengHei', Arial, sans-serif !important;
 }
 
+.vxe-tooltip {
+  background-color: #ff5722 !important; /* 设置背景颜色 */
+  color: #ffffff !important; /* 设置文字颜色 */
+  border-radius: 4px; /* 设置圆角 */
+}
+
 .iconfont_icon {
   margin-right: 2.42px;
 }

+ 135 - 7
src/styles/reset.scss

@@ -8,12 +8,6 @@ span,
 applet,
 object,
 iframe,
-h1,
-h2,
-h3,
-h4,
-h5,
-h6,
 p,
 blockquote,
 pre,
@@ -26,7 +20,6 @@ cite,
 code,
 del,
 dfn,
-em,
 img,
 ins,
 kbd,
@@ -141,3 +134,138 @@ div {
     margin: 0;
   }
 }
+div.markdown-body {
+  background: transparent;
+  table {
+    th,
+    td {
+      white-space: nowrap;
+    }
+  }
+}
+
+.query-style {
+  .markdown-body {
+    p {
+      color: #b5b9bf;
+    }
+  }
+}
+
+.markdown-body {
+  display: inline-block;
+  width: 100%;
+  color: #333;
+}
+
+div.markdown-body h1,
+div.markdown-body h2,
+div.markdown-body h3,
+div.markdown-body h4,
+div.markdown-body h5,
+div.markdown-body h6 {
+  margin: 1em 0 0.5em;
+  font-size: 14px;
+  font-weight: bold;
+}
+
+div.markdown-body h1 {
+  font-size: 16px;
+}
+.markdown-body p {
+  margin: 1em 0;
+}
+
+.markdown-body ul,
+.markdown-body ol {
+  margin-left: 1.5em;
+  padding-left: 1em;
+}
+
+div.markdown-body {
+  li,
+  th,
+  p,
+  td,
+  span {
+    font-size: 14px;
+  }
+}
+
+div.markdown-body ul {
+  list-style-type: disc;
+  margin-left: 8px;
+  padding-left: 16px;
+}
+
+.markdown-body ol {
+  list-style-type: decimal;
+  margin-left: 8px;
+  padding-left: 16px;
+}
+
+.markdown-body li {
+  margin: 0.5em 0;
+  & > ul {
+    margin-left: 0;
+  }
+  &:last-child {
+    margin-bottom: 0;
+  }
+}
+
+.markdown-body blockquote {
+  padding-left: 1em;
+  border-left: 4px solid #ddd;
+  color: #555;
+  margin: 1em 0;
+}
+
+div.markdown-body a {
+  color: var(--color-theme);
+  text-decoration: none;
+  & + a {
+    margin-left: 6px;
+  }
+}
+
+.markdown-body {
+  table {
+    td,
+    th {
+      text-align: left;
+    }
+  }
+}
+
+.markdown-body code {
+  background: #f5f5f5;
+  padding: 0.2em 0.4em;
+  border-radius: 4px;
+  font-family: monospace;
+  font-size: 0.9em;
+}
+
+.markdown-body pre code {
+  display: block;
+  padding: 1em;
+  background: #f6f8fa;
+  overflow-x: auto;
+}
+
+.markdown-test b,
+.markdown-test strong,
+.markdown-test p,
+.markdown-test code,
+.markdown-test blockquote,
+.markdown-test ul,
+.markdown-test ol,
+.markdown-test li,
+.markdown-test h1,
+.markdown-test h2,
+.markdown-test h3,
+.markdown-test h4,
+.markdown-test h5,
+.markdown-test h6 {
+  color: #fff;
+}

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

@@ -51,12 +51,14 @@
   --color-table-header-bg: #343a43;
   --icon-color-black: #fff;
   --more-filters-background-color: #2a2e34;
+  --add-rules-background-color: rgba(249,249,249,0.05);
   --addparties-background-color: #343a43;
   --color-border-top: #3f434a;
 
   // tag
   --tag-bg-color: rgba(239, 239, 240, 0.1);
   --tag-info-text-color: #fff;
+  --tag-boder-color: rgba(239, 239, 240, 0.1);
   --tips-bg-color: rgba(26, 28, 32, 1);
   --tag-info-bg-color: rgba(239, 239, 240, 0.1);
 
@@ -69,4 +71,29 @@
   --input-disabled-text-color: #66696f;
   --color-recent-name: rgba(240, 241, 243, 0.1);
   --color-disabled-bg: #2b2b2c;
+
+  --color-system-color-bg:#3f434a;
+  --color-system-border: #3f434a;
+  --color-system-checkbox-border: #656f7d;
+  --color-system-body-bg:#30353c;
+  --color-system-notification-bg:#343a43;
+  --color-system-card-bg:#2B2F36;
+  --color-system-border-1: #3F434A;
+  --color-system-input-border: #656f7d;
+
+  
+  // AI Robot
+  --color-dialogue-bg: rgba(255,255,255,0.13);
+  --color-dialogue_container-border: rgba(255,255,255,0.20);
+  --color-carousel-card-bg: rgba(255,255,255,0.70);
+  --color-dialogue-icon-bg: linear-gradient(92deg, var(--1-gradient-ai-robot-0, #525CBA) 1.33%, var(--1-gradient-ai-robot-100, #724493) 99.63%);
+  --color-dialogue-text-bg: linear-gradient(92deg, var(--1-gradient-ai-robot-0, #525CBA) 1.33%, var(--1-gradient-ai-robot-100, #724493) 99.63%);
+  --color-dialogue_container-bg:rgba(255, 255, 255, 0.10);
+  --color-dialogue_title: linear-gradient(90deg, var(--1-gradient-ai-robot-faq-0, #FFA8C7) 1.77%, var(--1-gradient-ai-robot-faq-46, #5988f3) 46.77%);
+  --color-dialogue_content-bg:linear-gradient(117deg, var(--1-gradient-ai-robot-0, #525CBA) 4.31%, var(--1-gradient-ai-robot-15, #5A57B2) 14.24%, var(--1-gradient-ai-robot-38, #5F54AD) 29.71%, var(--1-gradient-ai-robot-59, #664EA2) 43.72%, var(--1-gradient-ai-robot-83, #694CA0) 59.35%, var(--1-gradient-ai-robot-100, #724493) 70.56%);
+  --color-arrow-hoverL: rgb(252,238,227,0.4);
+  --color-prompt-preview-bg: #403844;
+  --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);
 }

+ 127 - 4
src/styles/theme.scss

@@ -76,8 +76,6 @@
   --color-border: #eaebed;
   --color-select-border: #eaebed;
   --border-color-2: #eaebed;
-  --color-border-1: #e8eaee;
-  --color-border-2: #eaebed;
 
   --color-mune-active-bg: #fdf5f1;
 
@@ -94,14 +92,17 @@
   --color-btn-default-dark-hover: #ff7500;
   // main-plain
   --color-btn-main-plain-bg-hover: hsl(26, 100%, 95%);
+  --color-btn-action-bg-hover: rgba(255, 117, 0, 0.2);
   // main
   --color-btn-main-bg-hover: #d56200;
+  --color-btn-main-bg-disabled: rgba(237, 109, 0, 0.3);
   // success
   --color-btn-success-bg-hover: #009765;
   // warning
   --color-btn-warning-bg: #e9b227;
   --color-btn-warning-bg-hover: #d2a023;
   // danger
+  --color-btn-danger-bg: #c9353f;
   --color-btn-danger-bg-hover: #b53039;
 
   --color-tag-checked-all: #fff2e8;
@@ -136,6 +137,7 @@
 
   --icon-color-black: #202020;
   --more-filters-background-color: #f9f9f9;
+  --add-rules-background-color: #f9f9f9;
   --addparties-background-color: #eff1f5;
 
   --badge__content--warning: #ed6d00;
@@ -167,6 +169,7 @@
 
   --color-range-text: #2b2f36;
   --tag-bg-color: rgba(239, 239, 240);
+  --tag-boder-color: #efeff0;
   --tips-bg-color: rgba(26, 28, 32, 1);
   --scoring-bg-color: #f2f4f7;
 
@@ -246,11 +249,74 @@
   --color-vxe-table-visited-row-bg: #f2f2f2;
 
   --color-public-tracking-empty-bg: #fff;
+  --color-dot-unchecked: #eeeeee;
+  --color-dot-checked: #ed6d00;
 
+  --color-system-message-nav-bg: #f6f6f6;
   --color-upload-file-bg: #fef8f2;
   --color-upload-file-color: #b5b9bf;
   --color-upload-file-border-bg: #f5b279;
 
+  --color-personal-preference-bg: #f5f7fa;
+
+  --color-password-card-first-bg: #ffe294;
+  --color-password-card-second-bg: #f6f8fa;
+  --color-password-expired-card-first-bg: #ef99a0;
+  --color-password-expired-card-second-bg: #f6f8fa;
+  --color-feature-card-first-bg: #fff4eb;
+  --color-feature-card-second-bg: #f0f3ff;
+  --color-feature-card-third-bg: #f0f3ff;
+
+  --color-milestone-tag-bg: #e6f1eb;
+  --color-delay-tag-bg: #f7e7e9;
+  --color-change-tag-bg: #f5f2e6;
+
+  --color-previous-bg: #e1e3e9;
+  --color-system-color-bg: #f6f8fa;
+  --color-system-border: #eaebed;
+  --color-system-checkbox-bg: #fff;
+  --color-system-checkbox-border: #eaebed;
+  --color-system-body-bg: #fff;
+  --color-system-notification-bg: #f6f8fa;
+  --color-system-card-bg: #fff;
+  --color-system-border-1: #e8eaee;
+  --color-system-input-border: #e8eaee;
+  --color-personal-preference-bg: #f5f7fa;
+
+  // AI Robot
+  --color-dialogue-bg: #fff;
+  --color-dialogue_container-border: #fff;
+  --color-carousel-card-bg: #fff;
+  --color-dialogue-icon-bg: radial-gradient(50% 43% at 50% 54.79%, #d5b4f3 0%, #fff9fc 100%);
+  --color-dialogue-text-bg: linear-gradient(92deg, #eaecff 1.33%, #f1e3fb 99.63%);
+  --color-dialogue_title: linear-gradient(90deg, #a71549 1.77%, #06256e 46.77%);
+  --color-dialogue_container-bg: rgba(255, 255, 255, 0.5);
+  --color-dialogue_content-bg: linear-gradient(
+    117deg,
+    var(--1-gradient-ai-robot-0, #b5fef3) 4.31%,
+    var(--1-gradient-ai-robot-15, #f9eeec) 14.24%,
+    var(--1-gradient-ai-robot-38, #fbd3ee) 29.71%,
+    var(--1-gradient-ai-robot-59, #ddd5f7) 43.72%,
+    var(--1-gradient-ai-robot-83, #c8f4f3) 59.35%,
+    var(--1-gradient-ai-robot-100, #cadff8) 70.56%
+  );
+  --color-ai-chat-header-bg-gradient-begin: #eaecff;
+  --color-ai-chat-header-bg-gradient-end: #fefdff;
+  --color-ai-user-bubble-bg-gradient-begin: #ffede6;
+  --color-ai-user-bubble-bg-gradient-end: #f2f4f7;
+  --input-border: #eaebed;
+  --color-pause-btn-bg: #fff1e6;
+  --color-arrow-hoverL: #fceee3;
+
+  --color-output-type-bg: #4361ed;
+  --color-output-type-string-bg: #6c757e;
+  --color-output-select-text: #f9a725;
+  --color-loading-text: #b5b9bf;
+  --color-warning-tips-bg: #fff4d1;
+  --color-prompt-preview-bg: #fcf7ff;
+  --color-prompt-diaolog-bg: #f8f9fd;
+  --color-prompt-disabled-bg: #f4f4f4;
+  --color-prompt-disabled-border: rgba(234, 235, 237, 0.3);
   --color-tour-popover-bg: #fff;
   --color-tour-prev-btn-border: #eaebed;
   --color-tour-next-btn-bg: #2b2f36;
@@ -307,7 +373,7 @@
   --color-user-config-title-bottom-border: #3f434a;
   --color-container-status-node-bg: #3e454f;
 
-  --color-btn-blue-bg: rgba(255, 255, 255, 0);
+  --color-btn-blue-bg: rgba(248, 249, 253, 0.1);
 
   --color-el-btn-pain-theme-border: #ed6d00;
   --color-el-btn-pain-theme-text: #ed6d00;
@@ -328,6 +394,8 @@
   --color-upload-file-bg: rgba(237, 109, 0, 0.2);
   --color-upload-file-color: rgba(240, 241, 243, 0.7);
   --color-upload-file-border-bg: rgba(237, 109, 0, 0.5);
+
+  --color-personal-preference-bg: #343a43;
   // 滚动条
   --color-scrollbar-thumb: #656f7d;
 
@@ -340,6 +408,21 @@
   --color-v-box-content-drag-bg: #2b2f36;
 
   --color-input-disabled-border: #656f7d;
+
+  --color-password-card-first-bg: #ffa000;
+  --color-password-card-second-bg: #d6c587;
+  --color-password-expired-card-first-bg: #f25a66;
+  --color-password-expired-card-second-bg: #cda9ac;
+  --color-feature-card-first-bg: #334181;
+  --color-feature-card-second-bg: #c651bd;
+  --color-feature-card-third-bg: #ffca6e;
+  --color-milestone-tag-bg: #3c5249;
+  --color-delay-tag-bg: #523942;
+  --color-change-tag-bg: #564f36;
+
+  --color-previous-bg: #454b54;
+
+  --color-system-message-nav-bg: #343a43;
   // 邮件
   --w-e-toolbar-bg-color: var(--color-email-bg);
   --w-e-textarea-bg-color: var(--color-email-bg);
@@ -371,6 +454,9 @@
   .el-input {
     --el-border: #656f7d;
   }
+  .el-radio {
+    --el-radio-input-border: #656f7d;
+  }
   --el-radio-input-border: #656f7d;
   --color-table-header-bg: #34383f;
   --el-input-focus: #2b2f36;
@@ -382,7 +468,7 @@
   }
   --color-el-popper-bg: #1a1c20;
   // --el-bg-color: var(--color-neutral-1);
-  --el-bg-color-overlay: #30353c;
+  --el-bg-color-overlay: #3e454f;
   div.el-popper.is-dark {
     --el-bg-color: var(--color-neutral-1);
   }
@@ -398,6 +484,43 @@
   --vxe-ui-input-border-color: #656f7d;
   --vxe-ui-table-menu-background-color: #3e454f;
   --color-vxe-table-visited-row-bg: #3c4149;
+  --vxe-ui-tooltip-dark-background-color: #3e454f;
+
+  button.el-button.el-button--pain-theme {
+    border: 1px solid var(--color-el-btn-pain-theme-border);
+    background-color: var(--color-el-btn-pain-theme-bg);
+
+    fill: var(--color-el-btn-pain-theme-text);
+    color: var(--color-el-btn-pain-theme-text);
+    span {
+      color: var(--color-el-btn-pain-theme-text);
+    }
+    &:hover {
+      border-color: var(--color-el-btn-pain-theme-border);
+      background-color: var(--color-el-btn-pain-theme-bg-hover);
+      color: var(--color-white);
+      fill: var(--color-white);
+    }
+  }
+
+  --color-ai-chat-header-bg-gradient-begin: #484f82;
+  --color-ai-chat-header-bg-gradient-end: #31363d;
+  --color-ai-user-bubble-bg-gradient-begin: #716763;
+  --color-ai-user-bubble-bg-gradient-end: #5c6a7d;
+  --input-border: #656f7d;
+  --color-pause-btn-bg: #453b36;
+  --color-loading-text: #818892;
+  --color-warning-tips-bg: #85681b;
+  --color-warning-tips-text: #edb82f;
+  div.markdown-body {
+    tr {
+      background-color: #30353c;
+    }
+    th,
+    td {
+      border-color: #3f434a;
+    }
+  }
 
   --color-tour-popover-bg: #cf5f00;
   --color-tour-prev-btn-border: #e6c5aa;

+ 3 - 1
src/styles/vxeTable.scss

@@ -90,7 +90,9 @@ div.vxe-table--render-default .vxe-cell--checkbox.is--indeterminate .vxe-checkbo
 div.vxe-table--render-default .vxe-cell--checkbox:not(.is--disabled):hover .vxe-checkbox--icon {
   color: var(--color-btn-main-bg-hover);
 }
-
+div.theme--dark .vxe-table--tooltip-content {
+  color: #fff;
+}
 // 表格tooltipzIndex
 .vxe-table--tooltip-wrapper.is--active {
   z-index: 9999 !important;

+ 2 - 6
src/utils/axios.ts

@@ -2,10 +2,7 @@ import axios, { type AxiosInstance, type AxiosRequestConfig, type AxiosResponse
 import router from '@/router'
 import { ElMessage, ElMessageBox } from 'element-plus'
 import { useUserStore } from '@/stores/modules/user'
-// import {
-//   showFullScreenLoading,
-//   tryHideFullScreenLoading
-// } from '@monorepo/shared/utils/serviceLoading'
+import emitter from '@/utils/bus'
 
 interface codeMessage {
   [key: number]: string
@@ -65,6 +62,7 @@ class HttpAxios {
           message: 'Please log in to use this feature.',
           grouping: true
         })
+        emitter.emit('login-out');
       } else if (response.data.code !== 200 && response.data.code !== 400) {
         ElMessageBox.alert(
           response.data?.data?.msg || 'The request failed. Please try again later',
@@ -156,5 +154,3 @@ class HttpAxios {
 }
 
 export default new HttpAxios({})
-
-// export default new HttpAxios({})

+ 27 - 0
src/utils/timezone.ts

@@ -0,0 +1,27 @@
+export const defaultTimeZone = (zone: any) => {
+  if(zone == 'zh-CN' || zone == 'zh-TW' || zone == 'zh-HK'|| zone == 'en-AU'|| zone == 'ms-MY' || zone == 'id-ID' ) {
+    return 'UTC+08'
+  } else if(zone == 'ja-JP' || zone == 'ko-KR') {
+    return 'UTC+09'
+  } else if(zone == 'en-US') {
+    return 'UTC-08'
+  } else if(zone == 'en-GB' || zone == 'pt-PT') {
+    return 'UTC+00'
+  } else if(zone == 'en-NZ' ) {
+    return 'UTC+12'
+  } else if(zone == 'de-DE' || zone == 'fr-FR' || zone == 'es-ES'|| zone == 'it-IT'|| zone == 'nl-NL'|| zone == 'pl-PL'|| zone == 'sv-SE'|| zone == 'da-DK'|| zone == 'no-NO') {
+    return 'UTC+01'
+  } else if(zone == 'ru-RU' || zone == 'fi-FI' || zone == 'he-IL'|| zone == 'el-GR') {
+    return 'UTC+02'
+  } else if(zone == 'es-MX') {
+    return 'UTC-06'
+  } else if(zone == 'pt-BR') {
+    return 'UTC-03'
+  } else if(zone == 'ar-SA' || zone == 'tr-TR') {
+    return 'UTC+03'
+  } else if(zone == 'hi-IN') {
+    return 'UTC+05:30'
+  } else if(zone == 'th-TH' || zone == 'vi-VN') {
+    return 'UTC+07'
+  }
+}

+ 105 - 10
src/utils/tools.ts

@@ -1,31 +1,56 @@
 import moment from 'moment-timezone'
+import { useUserStore } from '@/stores/modules/user'
 
-export const formatTimezone = (time: string, timezone: string) => {
+const userStore = useUserStore()
+const formatString = computed(() => {
+  return userStore.dateFormat || 'MM/DD/YYYY'
+})
+
+export const formatTimezone = (time: string, timezone?: string, is12HourClock?: boolean) => {
   if (!time) return '--'
   let formattedTime = ''
   if (time.length > 12) {
-    formattedTime = moment(time).format('MMM-DD-YYYY hh:mm A')
+    if (is12HourClock) {
+      // 如果是12小时制,使用12小时制格式化
+      formattedTime = moment(time).format(`${formatString.value} hh:mm A`)
+    } else {
+      // 如果是24小时制,使用24小时制格式化
+      formattedTime = moment(time).format(`${formatString.value} HH:mm`)
+    }
     if (!timezone) {
       return formattedTime
     }
-    let gmtOffset = ''
-    const timeZoneOffset = moment().tz(timezone).format('Z')
-    // 替换 "+07:00" 为 "GMT+07"
-    gmtOffset = `(GMT${timeZoneOffset.slice(0, 3)})`
-    return `${formattedTime} ${gmtOffset}`
+    let utcOffset = ''
+
+    const timeZoneOffset = moment.tz(time, timezone).format('Z')
+    // 替换 "+07:00" 为 "UTC+07"
+    utcOffset = `(UTC${timeZoneOffset.slice(0, 3)})`
+    return `${formattedTime} ${utcOffset}`
   } else {
-    formattedTime = moment(time).format('MMM-DD-YYYY')
+    formattedTime = moment(time).format(formatString.value)
     return formattedTime
   }
 }
 
+/**
+ * 返回传入地区的UTC时区格式化
+ * @param timezone
+ * @returns
+ */
+export const getTimezone = (timezone: string, time?: string): string => {
+  if (!timezone) return ''
+  const computedTime = time ? time : moment(time).format(formatString.value)
+  const offset = moment.tz(computedTime, timezone).format('Z')
+  return `UTC${offset.slice(0, 3)}`
+}
+
 export const formatTimezoneByUTCorGMT = (time: string, timezone: string) => {
   if (!time) return '--'
   let formattedTime = ''
-  formattedTime = moment(time).format('MMM-DD-YYYY hh:mm A')
+  formattedTime = moment(time).format(`${formatString.value} HH:mm`)
   let gmtOffset = ''
   if (timezone != null) {
-    const timeZoneOffset = moment().tz(timezone).format('Z')
+    const timeZoneOffset = moment.tz(time, timezone).format('Z')
     // 替换 "+07:00" 为 "GMT+07"
     if (timezone.includes('Seoul')) {
       gmtOffset = `(UTC${timeZoneOffset.slice(0, 3)})`
@@ -35,3 +60,73 @@ export const formatTimezoneByUTCorGMT = (time: string, timezone: string) => {
     return `${formattedTime} ${gmtOffset}`
   }
 }
+
+/**
+ * 判断是否是欧洲地区
+ */
+export const isEuropean = () => {
+  const userLanguage = navigator.language || 'en-US'
+  const europeanLocales = ['de', 'fr', 'it', 'es', 'nl', 'pl'] // 例:德语、法语、西班牙语等
+  return europeanLocales.some((locale) => userLanguage.startsWith(locale))
+}
+
+/**
+ * 根据传入的地区 格式化数字
+ * @param number - 需要格式化的数字
+ * @param digits - 小数位数
+ */
+export const formatNumber = (number: number, digits?: number): string => {
+  if (isNaN(number)) return '0'
+  const userLanguage = userStore.userInfo?.numbers_format === 'European' ? 'de-DE' : 'zh-CN'
+
+  // 设置数字格式化的选项
+  const options: Intl.NumberFormatOptions = {
+    style: 'decimal'
+    // minimumFractionDigits: digits
+    // maximumFractionDigits: 3
+  }
+  digits ?? (options.minimumFractionDigits = digits)
+  // 其他地区使用默认格式
+  return new Intl.NumberFormat(userLanguage, options).format(number)
+}
+
+/**
+ * 根据用户地区判断日期格式
+ * @returns {string} - 返回日期格式
+ */
+export const getDateFormat = () => {
+  const userLanguage = navigator.language || 'en-US' // 获取浏览器的语言设置
+  // 判断用户地区
+  if (userLanguage === 'en-US') {
+    return 'MM/DD/YYYY' // 美国使用 MM/DD/YYYY 格式
+  } else if (
+    userLanguage.startsWith('de') ||
+    userLanguage.startsWith('fr') ||
+    userLanguage.startsWith('it') ||
+    userLanguage.startsWith('es') ||
+    userLanguage.startsWith('pl') ||
+    userLanguage.startsWith('nl') ||
+    userLanguage.startsWith('pt') ||
+    userLanguage.startsWith('se')
+  ) {
+    return 'DD/MM/YYYY' // 其他欧洲国家(例:德语、法语、西班牙语等)使用 DD/MM/YYYY 格式
+  } else if (['zh-CN', 'ja-JP', 'ko-KR'].includes(userLanguage)) {
+    return 'YYYY-MM-DD' // 东亚国家(如简体中文、日语、韩语)使用 YYYY-MM-DD 格式
+  } else {
+    return 'DD/MM/YYYY' // 其他地区默认 DD/MM/YYYY 格式
+  }
+}
+
+/**
+ * 返回用户设备是否是Mac系统或者iPhone系统
+ * @returns {boolean}
+ */
+export const isMacOS = () => {
+  const userAgent = navigator.userAgent
+
+  if (userAgent.includes('iPhone') || userAgent.includes('iPad') || userAgent.includes('Mac')) {
+    return true
+  } else {
+    return false
+  }
+}

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

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

+ 195 - 0
src/views/AIApiLog/src/AIApiLog.vue

@@ -0,0 +1,195 @@
+<script lang="ts" setup>
+import { useCalculatingHeight } from '@/hooks/calculatingHeight'
+import TableView from './components/TableView'
+import dayjs from 'dayjs'
+
+const filterRef: Ref<HTMLElement | null> = ref(null)
+const containerHeight = useCalculatingHeight(document.documentElement, 290, [filterRef])
+const searchData = ref({
+  text_search: '',
+  request_date_start: '',
+  request_date_end: '',
+  ai_model: '',
+  response_duration_type: '',
+  response_duration_num: null
+})
+
+const aiModelList = [
+  {
+    label: 'Deepseek',
+    value: 'Deepseek'
+  },
+  {
+    label: 'Claude',
+    value: 'Claude'
+  }
+]
+
+const comparatorList = [
+  {
+    label: '>=',
+    value: 'thanOrEqual'
+  },
+  {
+    label: '=',
+    value: 'equal'
+  },
+  {
+    label: '<=',
+    value: 'lessOrEqual'
+  }
+]
+
+const tableRef = ref()
+
+const Search = () => {
+  tableRef.value.SearchOperationLog(searchData.value)
+}
+const DateChange = (date: any) => {
+  if (!date) {
+    searchData.value.request_date_start = ''
+    searchData.value.request_date_end = ''
+  } else {
+    searchData.value.request_date_start = dayjs(date[0]).format('MM/DD/YYYY')
+    searchData.value.request_date_end = dayjs(date[1]).format('MM/DD/YYYY')
+  }
+  tableRef.value.SearchOperationLog(searchData.value)
+}
+</script>
+<template>
+  <div class="dashboard">
+    <div class="Title">AI API Log</div>
+    <div class="display">
+      <div class="heaer_top">
+        <div class="input-tips_filter">
+          <el-input
+            placeholder="Search Request ID、Question ID"
+            v-model="searchData.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="date-tips_filter">
+          <CalendarDate @DateChange="DateChange"></CalendarDate>
+        </div>
+        <div class="tips_filter">
+          <el-select v-model="searchData.ai_model" clearable placeholder="AI Model">
+            <el-option
+              v-for="item in aiModelList"
+              :key="item.value"
+              :label="item.label"
+              :value="item.value"
+            />
+          </el-select>
+        </div>
+        <div class="comparator-tips_filter">
+          <span>Response Duration</span>
+          <el-select
+            placeholder=""
+            clearable
+            v-model="searchData.response_duration_type"
+            style="width: 70px; margin: 0 6px"
+          >
+            <el-option
+              v-for="item in comparatorList"
+              :key="item.value"
+              :label="item.label"
+              :value="item.value"
+            >
+            </el-option>
+          </el-select>
+          <el-input-number
+            v-model="searchData.response_duration_num"
+            placeholder=""
+            :controls="false"
+            :min="0"
+            style="width: 60px; height: 34px"
+          ></el-input-number>
+        </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;
+  height: 68px;
+  border-bottom: 1px solid var(--color-border);
+  font-size: var(--font-size-6);
+  font-weight: 700;
+  padding-left: 23.32px;
+  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;
+}
+:deep(.el-select__placeholder.is-transparent span) {
+  color: var(--tag-info-text-color) !important;
+}
+:deep(.ETD_title) {
+  margin-bottom: 0;
+}
+:deep(.ant-picker-range) {
+  width: 250px !important;
+  height: 32px;
+  background-color: var(--color-mode) !important;
+}
+.tips_filter {
+  flex: 1;
+  height: 30px;
+  max-width: 190px;
+  margin-right: 8px;
+}
+.input-tips_filter {
+  flex: 1;
+  max-width: 320px;
+  height: 32px;
+  margin-right: 8px;
+  :deep(.el-input__wrapper) {
+    height: 32px;
+  }
+}
+.date-tips_filter {
+  flex: 1;
+  max-width: 250px;
+  height: 32px;
+  margin-right: 8px;
+}
+.comparator-tips_filter {
+  flex: 1;
+  display: flex;
+  align-items: center;
+  max-width: 260px;
+  height: 32px;
+  margin-right: 8px;
+}
+.dashboard {
+  position: relative;
+  background-color: var(--color-mode);
+}
+:deep(.log_input .el-input__wrapper) {
+  box-shadow: 0 0 0 1px var(--color-select-border);
+  border-radius: 6px;
+}
+</style>

+ 91 - 0
src/views/AIApiLog/src/components/LogDialog.vue

@@ -0,0 +1,91 @@
+<script setup lang="ts">
+import VueJsonPretty from 'vue-json-pretty'
+import 'vue-json-pretty/lib/styles.css'
+
+const dialogVisible = ref(false)
+
+const requestContent = ref()
+const responseContent = ref()
+const requestContentRef = ref<HTMLElement | null>(null)
+const responseHeight = ref(580)
+const openDialog = (request, response) => {
+  dialogVisible.value = true
+  requestContent.value = request
+  responseContent.value = response
+  nextTick(() => {
+    if (requestContentRef.value) {
+      const height = requestContentRef.value.scrollHeight
+      responseHeight.value = 726 - height - 122
+    }
+  })
+}
+
+const clearData = () => {
+  requestContent.value = ''
+  responseContent.value = ''
+  responseHeight.value = 580
+}
+defineExpose({
+  openDialog
+})
+</script>
+
+<template>
+  <el-dialog
+    v-model="dialogVisible"
+    class="ai-api-log-dialog"
+    @closed="clearData"
+    title="AI API Log"
+    width="1000"
+    top="10vh"
+  >
+    <div class="request-section">
+      <div class="title">Request Content</div>
+      <div class="content" ref="requestContentRef">
+        {{ requestContent }}
+      </div>
+    </div>
+    <el-divider style="margin: 16px 0" />
+    <div class="response-section">
+      <div class="title">Response Content</div>
+      <div class="content" :style="{ height: responseHeight + 'px' }">
+        <vue-json-pretty :data="responseContent" :deep="4" />
+      </div>
+    </div>
+  </el-dialog>
+</template>
+
+<style lang="scss" scoped>
+.title {
+  margin-bottom: 8px;
+
+  font-size: 18px;
+  font-weight: 700;
+  line-height: 22px;
+}
+.content {
+  line-height: 21px;
+}
+.request-section {
+  padding: 16px;
+  padding-bottom: 0;
+}
+.response-section {
+  padding: 16px;
+  padding-top: 0;
+  .content {
+    padding: 8px 16px 20px 16px;
+    border-radius: 6px;
+    overflow: auto;
+    background-color: var(--color-share-link-bg);
+  }
+}
+</style>
+<style lang="scss">
+.ai-api-log-dialog {
+  height: 80%;
+  .el-dialog__body {
+    padding: 0;
+  }
+}
+</style>

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

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

+ 472 - 0
src/views/AIApiLog/src/components/TableView/src/TableView.vue

@@ -0,0 +1,472 @@
+<script setup lang="ts">
+import { ref, nextTick, onMounted } from 'vue'
+import { type VxeGridInstance, type VxeGridProps } from 'vxe-table'
+import DownloadDialog from './components/DownloadDialog.vue'
+import { autoWidth } from '@/utils/table'
+import { useRowClickStyle } from '@/hooks/rowClickStyle'
+import dayjs from 'dayjs'
+import { formatTimezone, formatNumber } from '@/utils/tools'
+import LogDialog from '../../LogDialog.vue'
+
+const props = defineProps({
+  height: {
+    type: Number,
+    default: 440
+  }
+})
+
+const tableOriginColumnsField = ref()
+const handleColumns = (columns: any, status?: string) => {
+  const newColumns = columns.map((item: any) => {
+    let curColumn: any = {
+      title: item.title,
+      field: item.field,
+      sortable: true,
+      minWidth: 120,
+      showOverflow: true
+    }
+    // 设置插槽
+    if (item.type === 'status' && status !== 'all') {
+      curColumn = {
+        ...curColumn,
+        slots: { default: 'status' }
+      }
+    } else if (item.type === 'link' && status !== 'all') {
+      curColumn = {
+        ...curColumn,
+        slots: { default: 'link' }
+      }
+    } else if (item.type === 'mode' && status !== 'all') {
+      curColumn = {
+        ...curColumn,
+        slots: { default: 'mode' },
+        formatter: ({ cellValue }: any) => {
+          return cellValue
+        }
+      }
+    }
+    // 格式化
+    if (item.formatter === 'date' || item.formatter === 'dateTime') {
+      curColumn = {
+        ...curColumn,
+        formatter: ({ cellValue }: any) => formatTimezone(cellValue)
+      }
+    }
+    return curColumn
+  })
+  return newColumns
+}
+
+// 获取表格列
+const getTableColumns = async () => {
+  tableLoadingColumn.value = true
+  await $api.getAIApiLogTableColumn().then((res: any) => {
+    if (res.code === 200) {
+      tableData.value.columns = [
+        { type: 'checkbox', width: 50, fixed: 'left' },
+        ...handleColumns(res.data.OperationTableColumns)
+      ]
+      tableOriginColumnsField.value = res.data.OperationTableColumns
+    }
+  })
+  nextTick(() => {
+    tableRef.value && autoWidth(tableData.value, tableRef.value)
+    tableLoadingColumn.value = false
+    selectedNumber.value = 0
+    selectedTableData.value = []
+  })
+}
+
+const pageInfo = ref({ pageNo: 1, pageSize: 20, total: 0 })
+const tempSearch = ref()
+// 获得表格数据后赋值
+const assignTableData = (data: any) => {
+  tableData.value.data = data.searchData || []
+  pageInfo.value.total = Number(data.rc) || 0
+  tempSearch.value = data.tmp_search
+  // 拥有所有字段的表格
+  setTimeout(() => {
+    allTable.value.columns = handleColumns(tableData.value.columns, 'all')
+    allTable.value.data = data.searchData || []
+    // 为了让导出的表格列宽度自适应
+    nextTick(() => {
+      allTableRef.value && autoWidth(allTable.value, allTableRef.value)
+    })
+  }, 1000)
+}
+
+let searchdata: any = {}
+// 获取表格数据
+const getTableData = async (isPageChange?: boolean) => {
+  const rc = isPageChange ? pageInfo.value.total : -1
+  tableLoadingTableData.value = true
+  await $api
+    .getAIApiLogTableData({
+      cp: pageInfo.value.pageNo,
+      ps: pageInfo.value.pageSize,
+      rc,
+      ...searchdata
+    })
+    .then((res: any) => {
+      if (res.code === 200) {
+        assignTableData(res.data)
+      }
+    })
+    .finally(() => {
+      selectedNumber.value = 0
+      selectedTableData.value = []
+      nextTick(() => {
+        tableRef.value && autoWidth(tableData.value, tableRef.value)
+        tableLoadingTableData.value = false
+      })
+    })
+}
+const SearchOperationLog = (val: any) => {
+  searchdata = val
+  tableLoadingTableData.value = true
+  $api
+    .getAIApiLogTableData({
+      cp: pageInfo.value.pageNo,
+      ps: pageInfo.value.pageSize,
+      rc: -1,
+      ...val
+    })
+    .then((res: any) => {
+      if (res.code === 200) {
+        assignTableData(res.data)
+      }
+    })
+    .finally(() => {
+      selectedNumber.value = 0
+      selectedTableData.value = []
+      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)'
+  },
+  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 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']
+  }
+})
+
+// 实现行点击样式
+useRowClickStyle(tableRef)
+
+const downloadDialogRef = ref()
+const handleDownload = () => {
+  const curSelectedColumns: string[] = []
+  tableRef.value?.columns?.forEach((item: any) => {
+    if (item.field) {
+      curSelectedColumns.push(item.title)
+    }
+  })
+  downloadDialogRef.value.openDialog(
+    curSelectedColumns,
+    selectedNumber.value || pageInfo.value.total
+  )
+}
+
+const exportLoading = ref(false)
+// 获取导出表格数据
+const getExportTableData = (status: number) => {
+  // 如果有选中表格行数据,那么只到处选中的数据
+  if (selectedNumber.value > 0) {
+    exportTable(status)
+    return
+  }
+  exportLoading.value = true
+  const buildColumnString = (columns: any[]): string => {
+    return columns
+      .filter((item) => item.field)
+      .map((item) => item.title)
+      .join(',')
+  }
+
+  let column = ''
+  if (status === 1) {
+    column = buildColumnString(tableData.value.columns)
+  } else {
+    column = buildColumnString(allTable.value.columns)
+  }
+  $api
+    .getAIApiLogAllTableData({
+      selected_fields: column,
+      tmp_search: tempSearch.value
+    })
+    .then((res: any) => {
+      if (res.code === 200) {
+        allTable.value.data = res.data.Data || []
+        nextTick(() => {
+          exportLoading.value = false
+          // 导出数据
+          exportTable(status)
+        })
+      }
+    })
+    .finally(() => {
+      exportLoading.value = false
+    })
+}
+// 导出表格 status: 1 导出当前表格 2 导出所有字段表格
+const exportTable = (status: number) => {
+  const exportConfig: any = {
+    type: 'xlsx',
+    message: false,
+    filename: `AI API Log_${dayjs().format('YYYYMMDDHH[h]mm[m]ss[s]')}`
+  }
+  if (status === 1) {
+    exportConfig.columnFilterMethod = ({ column }: any) => {
+      const index = tableData.value.columns.findIndex((item: any) => item.field === column.field)
+      // 排除复选框列
+      return column.field && index !== -1
+    }
+    exportConfig.columns = tableData.value.columns
+  }
+  if (selectedNumber.value > 0) {
+    exportConfig.dataFilterMethod = ({ row }: any) => {
+      const index = selectedTableData.value.findIndex(
+        (item: any) => item._X_ROW_KEY === row._X_ROW_KEY
+      )
+      return index !== -1
+    }
+  }
+  allTableRef.value?.exportData(exportConfig)
+}
+
+const tableLoadingColumn = ref(false)
+const tableLoadingTableData = ref(false)
+
+const selectedNumber = ref(0)
+const selectedTableData = ref([])
+// 复选框选中事件
+const handleCheckboxChange = ({ records }: any) => {
+  selectedNumber.value = records.length
+  selectedTableData.value = records
+}
+const handleCheckAllChange = ({ records }: any) => {
+  selectedNumber.value = records.length
+  selectedTableData.value = records
+}
+
+const logDialogRef = ref()
+const logLoading = ref(false)
+const handleLinkClick = (row) => {
+  logLoading.value = true
+  $api
+    .getAIApiLogDialog({
+      request_id: row['Request ID']
+    })
+    .then((res: any) => {
+      if (res.code === 200) {
+        logLoading.value = false
+        const data = res.data.Data
+        // 打开日志详情对话框
+        logDialogRef.value.openDialog(data.request_content, data.ai_response_content)
+      }
+    })
+}
+
+defineExpose({
+  SearchOperationLog
+})
+</script>
+
+<template>
+  <div
+    style="padding: 0px 20px"
+    class="table-box"
+    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="table-tools"
+      v-loading.fullscreen.lock="logLoading"
+      element-loading-text="Loading..."
+      element-loading-custom-class="element-loading"
+      element-loading-background="rgb(43, 47, 54, 0.7)"
+    >
+      <div class="left-total-records">{{ selectedNumber }} Selected</div>
+      <div class="right-tools-btn">
+        <el-button class="el-button--main el-button--pain-theme" @click="handleDownload">
+          <span style="margin-right: 8px" class="font_family icon-icon_download_b"></span>
+          Download
+        </el-button>
+      </div>
+    </div>
+    <vxe-grid
+      ref="tableRef"
+      v-vloading="tableLoadingTableData || tableLoadingColumn"
+      :height="props.height"
+      :style="{ border: 'none' }"
+      v-bind="tableData"
+      @checkbox-change="handleCheckboxChange"
+      @checkbox-all="handleCheckAllChange"
+    >
+      <!-- 空数据时的插槽 -->
+      <template #empty v-if="!tableLoadingTableData && tableData.data.length === 0">
+        <VEmpty></VEmpty>
+      </template>
+      <template #link="{ row, column }">
+        <span style="color: var(--color-theme); cursor: pointer" @click="handleLinkClick(row)">
+          {{ row[column.field] }}
+        </span>
+      </template>
+    </vxe-grid>
+    <vxe-grid :height="10" ref="allTableRef" class="all-table" v-bind="allTable"> </vxe-grid>
+    <div class="bottom-pagination">
+      <div class="left-total-records">Total {{ formatNumber(pageInfo.total) }}</div>
+      <div class="right-pagination">
+        <el-pagination
+          v-model:current-page="pageInfo.pageNo"
+          v-model:page-size="pageInfo.pageSize"
+          :page-sizes="[20, 50, 100, 150]"
+          :pager-count="3"
+          background
+          layout="sizes, prev, pager, next"
+          :total="pageInfo.total"
+          @size-change="getTableData(true)"
+          @current-change="getTableData(true)"
+        />
+      </div>
+    </div>
+    <DownloadDialog
+      @export="getExportTableData"
+      :isHideSelectColumn="true"
+      ref="downloadDialogRef"
+    />
+    <LogDialog ref="logDialogRef" />
+  </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 {
+  position: relative;
+  overflow: hidden;
+
+  .all-table {
+    position: absolute;
+    top: -100000px;
+    width: 20px;
+  }
+}
+</style>

+ 196 - 0
src/views/AIApiLog/src/components/TableView/src/components/DownloadDialog.vue

@@ -0,0 +1,196 @@
+<script setup lang="ts">
+const props = withDefaults(
+  defineProps<{
+    isHideSelectColumn: boolean
+  }>(),
+  { isHideSelectColumn: false }
+)
+const dialogVisible = ref(false)
+
+const openDialog = (selectedColumns: string[], slectedDataNumber: number) => {
+  selectedDataNumber.value = slectedDataNumber
+  columns.value = selectedColumns
+  dialogVisible.value = true
+}
+
+const isShowSelectColumn = ref(false)
+
+const downloadFilter = ref(1)
+const selectedDataNumber = ref(0)
+
+const columns = ref()
+
+const emits = defineEmits<{ export: [number] }>()
+const handleDownload = () => {
+  emits('export', downloadFilter.value)
+}
+
+const clearData = () => {
+  isShowSelectColumn.value = false
+  downloadFilter.value = 1
+}
+
+defineExpose({
+  openDialog,
+  handleDownload
+})
+</script>
+
+<template>
+  <div>
+    <el-dialog @close="clearData" v-model="dialogVisible" title="Download File" width="540">
+      <div class="download-dialog">
+        <div class="select-data">
+          <div style="display: inline-block">
+            Select data on your Opeartion Log list:<span style="color: var(--color-theme)">{{
+              selectedDataNumber
+            }}</span>
+          </div>
+        </div>
+        <div class="download-filter" v-if="!props.isHideSelectColumn">
+          <el-radio-group v-model="downloadFilter">
+            <el-radio :value="1"
+              >Download with selected columns
+              <span class="column-number">{{ columns.length }}</span>
+              <SeeAllIcon v-model="isShowSelectColumn" />
+            </el-radio>
+            <div
+              v-if="isShowSelectColumn"
+              class="select-columns"
+              :class="{ show: isShowSelectColumn }"
+            >
+              <div class="title">Selected columns</div>
+              <div class="content">
+                <div class="column-item" v-for="item in columns" :key="item">{{ item }}</div>
+              </div>
+            </div>
+            <el-radio :value="2">Download with all columns</el-radio>
+          </el-radio-group>
+        </div>
+      </div>
+      <template #footer>
+        <div class="dialog-footer">
+          <el-button class="cancel-btn" type="default" @click="dialogVisible = false"
+            >Cancel</el-button
+          >
+          <el-button class="download-btn el-button--dark" @click="handleDownload"
+            ><span style="margin-right: 8px" class="font_family icon-icon_download_b"></span>
+            Download</el-button
+          >
+        </div>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.download-dialog {
+  color: var(--color-neutral-1);
+}
+
+.select-data {
+  font-weight: 700;
+}
+
+.data-filter {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 8px;
+  max-height: 120px;
+  margin-top: 8px;
+  overflow: auto;
+
+  .filter-item {
+    height: 22px;
+    padding: 0px 8px;
+    background-color: var(--color-download-file-filter-tag-bg);
+    border-radius: 12px;
+    line-height: 22px;
+    font-size: 12px;
+  }
+}
+
+.download-filter {
+  margin-top: 16px;
+
+  .el-radio-group {
+    display: flex;
+    flex-direction: column;
+    align-items: flex-start;
+
+    .el-radio {
+      height: 40px;
+      align-items: center;
+    }
+
+    :deep(.el-radio__label) {
+      margin-top: 2px;
+      font-weight: 700;
+      color: var(--color-neutral-1);
+    }
+
+    .column-number {
+      padding: 3px 5px;
+      background-color: var(--color-theme);
+      border-radius: 12px;
+      font-size: 12px;
+      font-weight: 700;
+      color: #fff;
+    }
+
+    .see-all-btn {
+      margin-left: 8px;
+      color: var(--color-theme);
+      font-size: 12px;
+    }
+
+    .select-columns {
+      max-height: 350px;
+      padding: 8px;
+      margin-top: 8px;
+      background-color: var(--color-dialog-header-bg);
+      border-radius: 6px;
+      overflow: hidden;
+
+      &.show {
+        max-height: 500px;
+      }
+
+      .title {
+        font-size: 12px;
+        font-weight: 700;
+      }
+
+      .content {
+        display: flex;
+        flex-wrap: wrap;
+        margin-top: 8px;
+        gap: 8px;
+
+        .column-item {
+          height: 22px;
+          padding: 0px 8px;
+          background-color: var(--color-download-file-selected-column-tag-bg);
+          line-height: 22px;
+          border-radius: 12px;
+          font-size: 12px;
+        }
+      }
+    }
+  }
+}
+
+.dialog-footer {
+  .el-button {
+    height: 40px;
+  }
+
+  .cancel-btn {
+    width: 115px;
+  }
+
+  .download-btn {
+    width: 136px;
+  }
+}
+</style>

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

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

+ 848 - 0
src/views/AIRobotChat/src/AIRobotChat.vue

@@ -0,0 +1,848 @@
+<script setup lang="ts">
+import AutoResizeTextarea from './components/AutoResizeTextarea.vue'
+import LoadingDots from './components/LoadingDots.vue'
+import AIQuestions from './components/AIQuestions.vue'
+import userBubbleLight from './image/userBubbleLight.png'
+import userBubbleDark from './image/userBubbleDark.png'
+import robotBubbleLight from './image/robotBubbleLight.png'
+import robotBubbleDark from './image/robotBubbleDark.png'
+import { useThemeStore } from '@/stores/modules/theme'
+import { useUserStore } from '@/stores/modules/user'
+import MarkdownIt from 'markdown-it'
+import 'github-markdown-css/github-markdown.css'
+
+const userStore = useUserStore()
+const AIQuestion = ref()
+const md = new MarkdownIt({
+  html: true,
+  linkify: true,
+  typographer: true,
+  breaks: true
+})
+
+const renderedMessage = (content) => {
+  if (!content) {
+    return ''
+  }
+  const normalized = content
+    .replace(/\\n/g, '\n') // 粘贴带来的 \\n 处理
+    .replace(/\r\n/g, '\n') // Windows 换行标准化
+    .trim()
+  return md.render(normalized)
+}
+
+const themeStore = useThemeStore()
+const modalSize = ref('large')
+const userQuestion = ref()
+
+const isFooterInputFocus = ref(false)
+
+const userBubbleImg = computed(() => {
+  return themeStore.theme === 'light' ? userBubbleLight : userBubbleDark
+})
+const robotBubbleImg = computed(() => {
+  return themeStore.theme === 'light' ? robotBubbleLight : robotBubbleDark
+})
+
+// 是否显示footer的顶部阴影
+const isShowFooterShadow = ref(false)
+// 是否显示header的底部阴影
+const isShowHeaderShadow = ref(false)
+
+const loadingAnswer = ref(false) // 是否正在请求答案
+interface MessageItem {
+  id?: string // 唯一标识
+  type: 'robot' | 'user'
+  content: string
+  feedback?: 'Cood' | 'Not Good' | '' // 反馈结果
+  isShowFeedback?: boolean // 是否展示反馈样式
+  isAnswer?: boolean // 是否为用户问题的答案,是则才能展示反馈组件
+  isError?: boolean // 是否为错误消息
+  isCancel?: boolean // 是否为取消消息
+  html?: string // 渲染后的HTML内容
+}
+const messages = ref<MessageItem[]>([
+  {
+    type: 'robot',
+    isShowFeedback: false,
+    content: 'You can click on Frequently Asked Questions above or type your own question'
+  }
+])
+messages.value = JSON.parse(sessionStorage.getItem('AIChat')) || messages.value
+const handleOpen = () => {
+  scrollToBottom()
+}
+
+watch(
+  () => messages.value,
+  (newVal) => {
+    // 将消息存储到 sessionStorage 中
+    sessionStorage.setItem('AIChat', JSON.stringify(newVal))
+  },
+  { immediate: true, deep: true }
+)
+
+// 用户问题请求时间
+const queryTime = ref(-1)
+// 当前用户问题回复进度
+const progressStatus = {
+  init: 'You can click on Frequently Asked Questions above or type your own question',
+  '0': 'Thinking about your question...',
+  '15': 'Searching for relevant data, please wait...',
+  '30': 'This query is complex and may take more time',
+  '60': 'You may try simplifying your question or selecting a Frequently Asked Question',
+  '120': 'Sorry, the query failed. Please try again later or select a Frequently Asked Question',
+  cancel: 'You have stopped this answer'
+}
+
+const isShowTips = ref(false) // 是否展示提示信息
+
+const isShowLoadingDots = ref(false) // 是否展示加载点(加载答案)
+const parseHtmlString = (data) => {
+  if (!data) {
+    isShowLoadingDots.value = false // 停止显示加载点
+    return
+  }
+  const lines = data.split('\n')
+  isShowLoadingDots.value = true // 开始显示加载点
+  function streamMarkdown() {
+    const lastMsg: any = messages.value[messages.value.length - 1]
+    let index = 0
+    lastMsg.content = '' // 清空上一个消息的内容
+    const timer = setInterval(() => {
+      if (index < lines.length) {
+        lastMsg.content += lines[index] + '\n'
+        lastMsg.html = renderedMessage(lastMsg.content) // ✅ 每次整段渲染
+        index++
+        scrollToBottom() // 滚动到底部
+      } else {
+        isShowLoadingDots.value = false // 停止显示加载点
+        clearInterval(timer)
+      }
+    }, 150)
+  }
+
+  streamMarkdown()
+}
+
+const progressInterval = ref()
+const serial_no = ref()
+const is_FixedAnswer = ref(true) // 是否为预设问题 true是自由问题 false是预设问题
+const aiChat = (question, isPresetQuestion) => {
+  AIQuestion.value.AIRobotInit()
+  serial_no.value = userStore.userInfo?.uname + Date.now().toString()
+  let fixed_faq = ''
+  if (!is_FixedAnswer.value) {
+    fixed_faq = messages.value[messages.value.length - 3].content
+  } else if (isPresetQuestion) {
+    fixed_faq = question
+  }
+  $api
+    .aiChat({
+      serial_no: serial_no.value,
+      prompt: sessionStorage.getItem('prompt'),
+      question_type:
+        isPresetQuestion || !is_FixedAnswer.value ? 'Predefined Question' : 'Free Question',
+      question_content: question,
+      fixed_faq: fixed_faq
+    })
+    .then((res) => {
+      if (isPause.value || queryTime.value === -1) {
+        return
+      }
+      if (res.code === 200) {
+        clearInterval(progressInterval.value)
+
+        is_FixedAnswer.value =
+          res.data.is_fixedAnswer_end !== null || res.data.is_fixedAnswer_end !== undefined
+            ? res.data.is_fixedAnswer_end
+            : true
+        const { data } = res.data
+        messages.value[messages.value.length - 1] = {
+          id: serial_no.value,
+          type: 'robot',
+          content: '',
+          feedback: '',
+          isShowFeedback: false,
+          isAnswer: true
+        }
+
+        parseHtmlString(data)
+        scrollToBottom()
+        loadingAnswer.value = false
+        queryTime.value = -1
+      } else {
+        loadingAnswer.value = false
+        messages.value[messages.value.length - 1].isError = true
+        messages.value[messages.value.length - 1].content = progressStatus[120]
+        clearInterval(progressInterval.value)
+        queryTime.value = -1
+      }
+    })
+}
+const handleSend = (question, isPresetQuestion = true, isExternal = false) => {
+  if (!question) return
+  if (
+    isExternal &&
+    messages.value.length === 1 &&
+    messages.value[0].content === progressStatus.init
+  ) {
+    messages.value.pop()
+  }
+  if (loadingAnswer.value) {
+    isShowTips.value = true
+    setTimeout(() => {
+      isShowTips.value = false
+    }, 2000)
+    return
+  }
+  isPause.value = false
+  loadingAnswer.value = true
+  // 将用户内容添加到消息列表
+  messages.value.push({
+    type: 'user',
+    content: question,
+    isAnswer: true
+  })
+  !isPresetQuestion ? (userQuestion.value = '') : ''
+  queryTime.value = 0
+
+  aiChat(question, isPresetQuestion)
+  messages.value.push({
+    type: 'robot',
+    content: progressStatus[0]
+  })
+  autoScroll.value = true
+  scrollToBottom()
+
+  progressInterval.value = setInterval(() => {
+    queryTime.value++
+    // 定义时间点与对应状态的映射
+    const timeToStatusMap = {
+      15: 15,
+      30: 30,
+      60: 60
+    }
+    // 获取最后一个消息对象
+    const lastMessage = messages.value[messages.value.length - 1]
+    if (queryTime.value === 120) {
+      clearInterval(progressInterval.value)
+      lastMessage.content = progressStatus[120]
+      lastMessage.isError = true
+      queryTime.value = -1
+      loadingAnswer.value = false
+      return
+    }
+    if (timeToStatusMap[queryTime.value] !== undefined) {
+      lastMessage.content = progressStatus[queryTime.value]
+    }
+  }, 1000)
+}
+
+const showScrollButton = ref(false) // 控制按钮显示
+const messagesRef = ref()
+const autoScroll = ref(true)
+
+function handleScroll() {
+  const el = messagesRef.value
+  const threshold = 50 // 到底部的距离阈值
+  const atBottom = el.scrollHeight - el.scrollTop - el.clientHeight < threshold
+  autoScroll.value = atBottom
+
+  showScrollButton.value = el.scrollHeight > el.clientHeight && !autoScroll.value
+
+  isShowFooterShadow.value =
+    messagesRef.value?.scrollTop <
+    messagesRef.value?.scrollHeight - messagesRef.value?.clientHeight - 1
+
+  isShowHeaderShadow.value = !!messagesRef.value?.scrollTop
+}
+const scrollToBottom = (isScroll = false) => {
+  if (!isScroll && (!autoScroll.value || !messagesRef.value)) return
+  nextTick(() => {
+    if (messagesRef.value) {
+      messagesRef.value.scrollTop = messagesRef.value.scrollHeight
+    }
+  })
+}
+
+const isPause = ref(false) // 是否暂停回答
+// 暂停回答
+const handlePause = () => {
+  $api
+    .pauseAiChat({
+      serial_no: serial_no.value
+    })
+    .then((res) => {
+      if (res.code === 200) {
+        clearInterval(progressInterval.value)
+        is_FixedAnswer.value = true
+        queryTime.value = -1
+        messages.value[messages.value.length - 1].isCancel = true
+        messages.value[messages.value.length - 1].content = progressStatus.cancel
+        isPause.value = true
+        loadingAnswer.value = false
+        scrollToBottom()
+      }
+    })
+}
+
+// 评价
+const handleFeedback = (index, feedback) => {
+  const message = messages.value[index]
+  message.feedback = feedback
+  $api
+    .feedbackAiChat({
+      serial_no: message.id,
+      feedback: feedback
+    })
+    .then((res) => {
+      if (res.code === 200) {
+        messages.value[index].isShowFeedback = false
+      }
+    })
+}
+// 判断是否展示评价icon
+const shouldShowFeedback = (msg, index) => {
+  if (index === messages.value.length - 1 && isShowLoadingDots.value) {
+    return false
+  }
+  return msg.type === 'robot' && msg.isAnswer
+}
+
+const emit = defineEmits(['close'])
+// 关闭聊天窗口
+const handleClose = () => {
+  // progressInterval.value && clearInterval(progressInterval.value)
+  emit('close')
+}
+
+const clearData = () => {
+  messages.value = [
+    {
+      type: 'robot',
+      isShowFeedback: false,
+      content: 'You can click on Frequently Asked Questions above or type your own question'
+    }
+  ]
+  progressInterval.value && clearInterval(progressInterval.value)
+  sessionStorage.removeItem('AIChat')
+}
+
+const liabilityExeDialog = ref(false) // 免责声明弹窗
+const handleLiabilityExeDialog = () => {
+  liabilityExeDialog.value = true
+}
+
+const handelclckaiinit = () => {
+  AIQuestion.value.AIRobotInit() 
+}
+
+defineExpose({
+  handleSend,
+  handleOpen,
+  clearData,
+  handelclckaiinit
+})
+
+</script>
+
+<template>
+  <div class="ai-robot" :style="{ width: modalSize === 'large' ? '1000px' : '484px' }">
+    <div class="top-section" :class="[isShowHeaderShadow ? 'box-shadow' : '']">
+      <div class="header">
+        <span class="welcome">Hi! I'm your Freight Assistant</span>
+        <div class="option-icon">
+          <el-tooltip v-if="modalSize === 'large'" trigger="hover" content="Sidebar Window">
+            <el-button
+              style="width: 24px; height: 24px"
+              class="el-button--text"
+              @click="modalSize = 'small'"
+              ><span class="font_family icon-icon_sidebar__window_b"></span
+            ></el-button>
+          </el-tooltip>
+
+          <el-tooltip v-else-if="modalSize !== 'large'" trigger="hover" content="Maximized Window">
+            <el-button
+              style="width: 24px; height: 24px"
+              class="el-button--text"
+              @click="modalSize = 'large'"
+              ><span class="font_family icon-icon_maximized__window_b"></span
+            ></el-button>
+          </el-tooltip>
+
+          <el-tooltip trigger="hover" content="Collapsed to Widget">
+            <el-button
+              style="width: 24px; height: 24px"
+              class="el-button--text"
+              @click="handleClose"
+              ><span
+                @click="handleClose"
+                class="font_family icon-icon_collapsed__to_widget_b"
+              ></span
+            ></el-button>
+          </el-tooltip>
+        </div>
+      </div>
+      <AIQuestions ref="AIQuestion" :modalSize="modalSize" @question="handleSend"></AIQuestions>
+      <div class="warning-tips" v-if="isShowTips">
+        <div class="warning-bg">
+          <span class="warning-icon font_family icon-icon_warning_fill_b"></span>
+        </div>
+        <span>Answer in progress, please wait.</span>
+      </div>
+    </div>
+    <div class="chat-messages" ref="messagesRef" @scroll="handleScroll">
+      <div
+        class="message-item"
+        :class="[
+          msg.type === 'user' ? 'user-bubble' : 'robot-bubble',
+          (queryTime > -1 && queryTime < 120 && index === messages.length - 1) || msg.isCancel
+            ? 'query-style'
+            : ''
+        ]"
+        :style="{
+          maxWidth: modalSize === 'large' ? 'calc(100% - 20px)' : 'calc(100% - 16px)'
+        }"
+        v-for="(msg, index) in messages"
+        :key="index"
+        @mouseenter="msg.isShowFeedback = true"
+        @mouseleave="msg.isShowFeedback = false"
+      >
+        <!-- 请求失败后的提示icon   -->
+        <div class="pause-bg" v-if="msg.isError">
+          <span class="pause-icon font_family icon-icon_warning_fill_b"></span>
+        </div>
+        <!-- loading icon -->
+        <img
+          class="loading-img"
+          v-if="queryTime > -1 && queryTime < 120 && index === messages.length - 1"
+          src="./image/icon_loading.png"
+          alt=""
+        />
+        <div style="display: inline-block; max-width: 100%">
+          <div v-html="msg.html || renderedMessage(msg.content)" class="markdown-body"></div>
+        </div>
+        <LoadingDots
+          v-if="
+            index === messages.length - 1 &&
+            msg.isAnswer &&
+            isShowLoadingDots &&
+            msg.type === 'robot'
+          "
+        ></LoadingDots>
+        <!-- 评价  -->
+        <div class="review" v-if="shouldShowFeedback(msg, index)">
+          <el-button
+            v-if="msg.feedback !== 'Cood'"
+            class="el-button--text"
+            @click="handleFeedback(index, 'Cood')"
+          >
+            <span class="font_family icon-icon_good_b"></span>
+          </el-button>
+          <div v-if="msg.feedback === 'Cood'" style="width: 16px; text-align: center">
+            <span
+              style="color: var(--color-theme)"
+              class="font_family icon-icon_good__filled_b"
+            ></span>
+          </div>
+          <el-button
+            v-if="msg.feedback !== 'Not Good'"
+            class="el-button--text"
+            @click="handleFeedback(index, 'Not Good')"
+          >
+            <span class="font_family icon-icon_notgood_b"></span>
+          </el-button>
+          <div v-if="msg.feedback === 'Not Good'" style="width: 16px; text-align: center">
+            <span
+              style="color: var(--color-theme)"
+              class="font_family icon-icon_notgood__filled_b"
+            ></span>
+          </div>
+        </div>
+
+        <img class="robot-bubble-img" v-if="msg.type === 'robot'" :src="robotBubbleImg" alt="" />
+        <img class="user-bubble-img" v-else-if="msg.type === 'user'" :src="userBubbleImg" alt="" />
+        <!-- 暂停回答 icon -->
+        <el-tooltip
+          v-if="index === messages.length - 1 && queryTime > 29 && queryTime < 120"
+          content="Cancel Answer"
+          placement="bottom-start"
+          effect="dark"
+          ><div class="pause-btn" @click="handlePause">
+            <div class="dot"></div>
+          </div>
+        </el-tooltip>
+      </div>
+      <!-- 滚动到底部icon -->
+      <div v-if="showScrollButton" class="scroll-to-bottom-btn" @click="scrollToBottom(true)">
+        <span class="font_family icon-icon_movedown_b"></span>
+      </div>
+    </div>
+
+    <div
+      class="footer"
+      :class="[isFooterInputFocus ? 'focus-style' : '', isShowFooterShadow ? 'box-shadow' : '']"
+    >
+      <div class="footer-input">
+        <AutoResizeTextarea
+          style="flex: 1"
+          v-model="userQuestion"
+          :placeholder="'Type your question here...'"
+          @focus="isFooterInputFocus = true"
+          @blur="isFooterInputFocus = false"
+        />
+        <div
+          class="input-icon"
+          :class="[!userQuestion || queryTime !== -1 ? 'disable' : '']"
+          @click="handleSend(userQuestion, false)"
+        >
+          <span class="font_family icon-icon_send_b"></span>
+        </div>
+      </div>
+      <div class="liability-exemption">
+        <span>Content is generated by Al, please check carefully!</span>
+        <span class="liability-exemption-btn" @click="handleLiabilityExeDialog">Disclaimer</span>
+      </div>
+    </div>
+    <el-dialog
+      class="liability-exemption-dialog"
+      v-model="liabilityExeDialog"
+      title="Disclaimer"
+      width="800"
+      top="20vh"
+    >
+      <div
+        class="title"
+        style="margin-bottom: 8px; font-size: 16px; font-weight: 700; line-height: 24px"
+      >
+        Important Notice: AI-Generated Content
+      </div>
+      <p>
+        This chat assistant is powered by artificial intelligence (AI) and is designed to help you
+        access information and answer your queries efficiently. Please be aware of the following:
+      </p>
+      <p>
+        <strong>• AI-Generated Responses: </strong>All responses are automatically generated by AI.
+        While we strive for accuracy, errors or inaccuracies may occur. Please verify critical
+        information independently before making business decisions.
+      </p>
+      <p>
+        <strong>• Data Privacy & Security:</strong> You can only access shipment data within your
+        authorized account permissions. Your data remains confidential and will not be shared with
+        other users or third parties.
+      </p>
+      <p>
+        <strong>• Information Accuracy: </strong> For critical business decisions or time-sensitive
+        matters, we recommend contacting our customer service team directly for verification.
+      </p>
+      <p>
+        <strong>• Service Limitations: </strong> This assistant provides general guidance and data
+        queries. For complex or specialized requests, please reach out to our support team.
+      </p>
+      <p>
+        By using this AI assistant, you acknowledge these limitations and agree to use the
+        information provided accordingly.
+      </p>
+    </el-dialog>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.ai-robot {
+  position: absolute;
+  top: 24px;
+  right: 24px;
+  height: calc(100% - 48px);
+  z-index: 2000;
+  display: flex;
+  flex-direction: column;
+  border-radius: 12px;
+  border: 1px solid var(--color-border);
+  box-shadow: 4px 4px 32px 0px rgba(0, 0, 0, 0.2);
+  background-color: var(--color-dialog-body-bg);
+  overflow: hidden;
+  .top-section {
+    position: relative;
+    padding-bottom: 16px;
+    background: linear-gradient(
+      to bottom,
+      var(--color-ai-chat-header-bg-gradient-begin) 10%,
+      var(--color-ai-chat-header-bg-gradient-begin) 10%,
+      var(--color-ai-chat-header-bg-gradient-end) 100%
+    );
+    &.box-shadow {
+      box-shadow: -2px 0px 12px rgba(0, 0, 0, 0.1);
+    }
+    .header {
+      display: flex;
+      justify-content: space-between;
+      align-items: center;
+      height: 64px;
+      margin-bottom: 6px;
+      padding: 0 16px;
+      .welcome {
+        font-size: 18px;
+        font-weight: 700;
+      }
+      .option-icon {
+        display: flex;
+        align-items: center;
+        .font_family {
+          font-size: 16px;
+          cursor: pointer;
+          &:hover {
+            color: var(--color-theme);
+          }
+        }
+      }
+    }
+    .warning-tips {
+      position: absolute;
+      top: 60px;
+      left: 17px;
+      width: calc(100% - 32px);
+      height: 40px;
+      padding-top: 14px;
+      background-color: var(--color-warning-tips-bg);
+      border-radius: 6px;
+      text-align: center;
+      .warning-bg {
+        position: relative;
+        display: inline-flex;
+        align-items: center;
+        justify-content: center;
+        height: 12px;
+        width: 12px;
+        margin-right: 6px;
+        border-radius: 50%;
+        background-color: #fff;
+      }
+      .warning-icon {
+        position: absolute;
+        top: -3px;
+        border-radius: 50%;
+        border: none;
+      }
+      span {
+        color: #edb82f;
+      }
+    }
+  }
+
+  .chat-messages {
+    flex: 1;
+    display: flex;
+    flex-direction: column;
+    gap: 16px;
+    padding: 0 16px 18px;
+    overflow: auto;
+    .message-item {
+      position: relative;
+      display: inline-block;
+      padding: 12px 16px;
+      margin-bottom: 7px;
+      border-radius: 12px;
+      background-color: var(--scoring-bg-color);
+      .review {
+        display: flex;
+        align-items: center;
+        gap: 16px;
+        margin-top: 12px;
+
+        button.el-button + .el-button {
+          margin-left: 0px;
+        }
+      }
+      .pause-bg {
+        position: relative;
+        display: inline-flex;
+        align-items: center;
+        justify-content: center;
+        height: 12px;
+        width: 12px;
+        margin-right: 6px;
+        border-radius: 50%;
+        background-color: #fff;
+      }
+      .pause-icon {
+        position: absolute;
+        top: -2px;
+        color: #c9353f;
+        border-radius: 50%;
+        border: none;
+      }
+      .el-button--text {
+        height: 16px;
+        width: 16px;
+        span {
+          color: var(--color-neutral-2);
+        }
+        &:hover {
+          span {
+            color: var(--color-theme);
+          }
+        }
+      }
+
+      .loading-img {
+        width: 16px;
+        height: 16px;
+        margin-top: -1px;
+        margin-right: 4px;
+        animation: loading-rotate 2s linear infinite;
+      }
+
+      .pause-btn {
+        position: absolute;
+        right: -22px;
+        top: 50%;
+        transform: translateY(-50%);
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        height: 16px;
+        width: 16px;
+        border-radius: 50%;
+        background-color: var(--color-pause-btn-bg);
+        .dot {
+          height: 5px;
+          width: 5px;
+          border-radius: 1px;
+          background-color: var(--color-theme);
+        }
+      }
+    }
+
+    .robot-bubble {
+      background: var(--scoring-bg-color);
+      align-self: flex-start;
+      .robot-bubble-img {
+        position: absolute;
+        left: -1px;
+        bottom: -7px;
+      }
+    }
+    .user-bubble {
+      align-self: flex-end;
+      background: linear-gradient(
+        to right,
+        var(--color-ai-user-bubble-bg-gradient-begin),
+        var(--color-ai-user-bubble-bg-gradient-end)
+      );
+      .user-bubble-img {
+        position: absolute;
+        right: 0;
+        bottom: -7px;
+      }
+    }
+    .scroll-to-bottom-btn {
+      position: absolute;
+      right: 50%;
+      transform: translateX(50%);
+      bottom: 76px;
+      display: flex;
+      justify-content: center;
+      align-items: center;
+      height: 32px;
+      width: 32px;
+      border-radius: 50%;
+      // background-color: #f5f4f4;
+      background: rgba(255, 255, 255, 0.6); /* 半透明背景色 */
+      box-shadow: 2px 2px 12px 0px rgba(0, 0, 0, 0.3);
+      backdrop-filter: blur(1.7px); /* 应用10px的模糊效果 */
+      span {
+        color: #2b2f36;
+      }
+      cursor: pointer;
+      &:hover {
+        background-color: #fceee3;
+        span {
+          color: var(--color-theme);
+        }
+      }
+    }
+  }
+  .footer {
+    padding: 12px 16px;
+    &.box-shadow {
+      box-shadow: 0px 2px 12px rgba(0, 0, 0, 0.1);
+    }
+    &.focus-style {
+      .footer-input {
+        border: 1px solid var(--color-theme);
+      }
+      .input-icon {
+        background-color: var(--color-theme);
+        span {
+          color: #fff;
+        }
+        &:hover {
+          background-color: #d56200;
+        }
+      }
+    }
+  }
+  .footer-input {
+    display: flex;
+    align-items: flex-end;
+    gap: 12px;
+    padding: 4px 12px;
+    padding-right: 4px;
+    border: 1px solid var(--input-border);
+    border-radius: 20px;
+    box-sizing: border-box;
+
+    .input-icon {
+      display: flex;
+      justify-content: center;
+      align-items: center;
+      height: 32px;
+      width: 32px;
+      padding: 1px 0 0 2px;
+      border-radius: 50%;
+      cursor: pointer;
+      &.disable {
+        cursor: not-allowed;
+      }
+    }
+  }
+  .liability-exemption {
+    margin-top: 8px;
+    font-size: 12px;
+    text-align: center;
+    span {
+      color: var(--color-neutral-3);
+    }
+    .liability-exemption-btn {
+      margin-left: 2px;
+      text-decoration: underline;
+      color: var(--color-theme);
+      cursor: pointer;
+    }
+  }
+
+  @keyframes loading-rotate {
+    0% {
+      transform: rotate(0deg);
+    }
+
+    100% {
+      transform: rotate(360deg);
+    }
+  }
+}
+:deep(.liability-exemption-dialog) {
+  padding-bottom: 0;
+  .el-dialog__body {
+    padding-top: 8px;
+    p {
+      margin-bottom: 8px;
+      line-height: 21px;
+      &:last-child {
+        margin-bottom: 0;
+      }
+    }
+  }
+}
+</style>

+ 250 - 0
src/views/AIRobotChat/src/components/AIQuestions.vue

@@ -0,0 +1,250 @@
+<script setup lang="ts">
+import { ref, onMounted } from 'vue'
+
+const props = defineProps<{
+  modalSize: String
+}>()
+
+const DeQuestions = ref([])
+const itemGroups = ref([])
+const pages = ref([])
+
+const AIRobotInit = () => {
+  $api.AIRobotInit({}).then((res:any) => {
+    DeQuestions.value = res.data.fixed_question
+    prepareGroups()
+    generatePages()
+  })
+}
+// 随机显示方法
+const prepareGroups = () => {
+  const groups = []
+  let currentGroup = []
+  let currentHeight = 0
+  DeQuestions.value.forEach((item) => {
+    const itemHeight = item.isLong ? 2 : 1
+
+    if (currentHeight + itemHeight > 4) {
+      groups.push(currentGroup)
+      currentGroup = []
+      currentHeight = 0
+    }
+
+    currentGroup.push(item)
+    currentHeight += itemHeight
+  })
+
+  // 添加最后一组
+  if (currentGroup.length > 0) {
+    groups.push(currentGroup)
+  }
+  itemGroups.value = groups
+}
+// 智能分页算法
+const generatePages = () => {
+  const result = []
+  let currentPage = { row1: [], row2: [] }
+  let cursor = 0
+
+  while (cursor < DeQuestions.value.length) {
+    // 处理第一排
+    const currentItem = DeQuestions.value[cursor]
+    // 第一排逻辑
+    if (currentItem.isLong) {
+      // 长文本独占第一排
+      currentPage.row1.push(currentItem)
+      cursor++
+    } else {
+      // 尝试取两个短文本
+      const nextItem = DeQuestions.value[cursor + 1]
+      if (nextItem && !nextItem.isLong) {
+        currentPage.row1.push(currentItem, nextItem)
+        cursor += 2
+      } else {
+        currentPage.row1.push(currentItem)
+        cursor++
+      }
+    }
+
+    // 处理第二排
+    const remaining = DeQuestions.value.slice(cursor)
+    if (remaining.length > 0) {
+      const secondItem = remaining[0]
+
+      if (secondItem.isLong) {
+        currentPage.row2.push(secondItem)
+        cursor++
+      } else {
+        const nextSecondItem = remaining[1]
+        if (nextSecondItem && !nextSecondItem.isLong) {
+          currentPage.row2.push(secondItem, nextSecondItem)
+          cursor += 2
+        } else {
+          currentPage.row2.push(secondItem)
+          cursor++
+        }
+      }
+    }
+
+    // 保存当前页
+    result.push(currentPage)
+
+    // 重置页面
+    currentPage = { row1: [], row2: [] }
+  }
+  pages.value = result.filter((p) => p.row1.length > 0)
+}
+onMounted(() => {
+  AIRobotInit()
+})
+
+defineExpose({
+  AIRobotInit
+})
+
+const emit = defineEmits<{ question: [string] }>()
+const clickQuestion = (question) => {
+  emit('question', question)
+}
+</script>
+<template>
+  <div class="flex_center">
+    <div class="dialogue_content">
+      <div class="dialogue_content_title">
+        <div class="dialogue_title_left">
+          <img
+            class="dialogue_title_left_img"
+            src="../image/icon_ai_robot48_b@2x.png"
+            width="48px"
+          />
+          <div class="dialogue_title_left_text">Frequently Asked Questions</div>
+        </div>
+      </div>
+      <el-carousel
+        v-if="props.modalSize === 'large'"
+        class="carousel large_carousel"
+        :autoplay="false"
+        height="105px"
+      >
+        <el-carousel-item v-for="(page, index) in pages" :key="index">
+          <div class="dialogue_container dialogue_container_large">
+            <div class="double-row-layout">
+              <!-- 第一排 -->
+              <div class="row first-row">
+                <template v-for="item in page.row1" :key="item.id">
+                  <div
+                    @click="clickQuestion(item.label)"
+                    class="dialogue_content_item"
+                    :class="{ 'long-item': item.isLong }"
+                  >
+                    {{ item.label }}
+                  </div>
+                </template>
+              </div>
+              <!-- 第二排 -->
+              <div class="row second-row">
+                <template v-for="item in page.row2" :key="item.id">
+                  <div
+                    @click="clickQuestion(item.label)"
+                    class="dialogue_content_item"
+                    :class="{ 'long-item': item.isLong }"
+                  >
+                    {{ item.label }}
+                  </div>
+                </template>
+              </div>
+            </div>
+          </div>
+        </el-carousel-item>
+      </el-carousel>
+      <el-carousel v-else class="carousel small_carousel" :autoplay="false" height="190px">
+        <el-carousel-item v-for="(group, index) in itemGroups" :key="index">
+          <div class="dialogue_container">
+            <div
+              class="dialogue_content_item"
+              v-for="item in group"
+              :key="item.label"
+              @click="clickQuestion(item.label)"
+              :class="{ 'long_item': item.isLong }"
+            >
+              {{ item.label }}
+            </div>
+          </div>
+        </el-carousel-item>
+      </el-carousel>
+    </div>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.dialogue_content {
+  background: var(--color-dialogue_content-bg);
+}
+.dialogue_title_left {
+  position: relative;
+}
+.dialogue_title_left_img {
+  position: absolute;
+  top: -20px;
+}
+.dialogue_title_left_text {
+  background: var(--color-dialogue_title);
+  -webkit-background-clip: text;
+  background-clip: text;
+  color: transparent;
+  font-size: 14px;
+  font-weight: 700;
+  margin: 1px 0 0 55px;
+}
+.small_carousel {
+  width: 452px;
+}
+.large_carousel {
+  width: 968px;
+}
+.double-row-layout {
+  height: 100%;
+  display: flex;
+  flex-direction: column;
+  gap: 4px;
+}
+.row {
+  flex: 1;
+  display: flex;
+  min-height: 0; /* 防止内容溢出 */
+}
+.dialogue_content_item:hover {
+  background-color: var(--color-arrow-hoverL);
+  color: var(--color-theme);
+}
+.long-item {
+  grid-column: span 2;
+}
+.dialogue_container_large {
+  width: 936px;
+  height: 84px;
+}
+.dialogue_content_item {
+  width: 100%;
+  height: 32px;
+  padding: 5.5px 8px;
+  border-radius: 6px;
+  background-color: var(--color-dialogue-bg);
+  margin-right: 4px;
+  word-break: break-word;
+  overflow: hidden;
+  display: flex;
+  align-items: center;
+  justify-content: start;
+  text-align: center;
+  cursor: pointer;
+  min-width: 0; /* 防止文本溢出 */
+}
+:deep(.el-carousel__item--card, .el-carousel__item.is-animating) {
+  padding-bottom: 0 !important;
+}
+:deep(.el-carousel__indicators--horizontal) {
+  bottom: -2px;
+  height: 16px;
+}
+</style>

+ 83 - 0
src/views/AIRobotChat/src/components/AutoResizeTextarea.vue

@@ -0,0 +1,83 @@
+<script setup lang="ts">
+const inputVModel = defineModel({ type: String })
+const props = defineProps<{
+  placeholder?: string
+}>()
+
+watch(
+  () => inputVModel.value,
+  () => {
+    textareaRef.value.style.height = '30px' // 重置高度
+  }
+)
+const textareaRef = ref(null)
+const isComposing = ref(false)
+
+// 实现自适应高度(最多 4 行)
+const resize = () => {
+  const el = textareaRef.value
+  if (!el) return
+  el.style.height = 'auto' // 先清空旧高度
+  const scrollHeight = el.scrollHeight
+  const maxHeight = 92 // 四行时高度
+
+  if (scrollHeight <= maxHeight) {
+    el.style.overflowY = 'hidden'
+    el.style.height = scrollHeight + 'px'
+  } else {
+    el.style.overflowY = 'auto'
+    el.style.height = maxHeight + 'px'
+  }
+}
+
+// 处理中文输入法
+const handleCompositionStart = () => {
+  isComposing.value = true
+}
+
+const handleCompositionEnd = () => {
+  isComposing.value = false
+  nextTick(resize)
+}
+
+const emit = defineEmits(['focus', 'blur'])
+</script>
+
+<template>
+  <textarea
+    ref="textareaRef"
+    v-model="inputVModel"
+    class="input-area"
+    rows="1"
+    :placeholder="props.placeholder"
+    @input="!isComposing && resize()"
+    @compositionstart="handleCompositionStart"
+    @compositionend="handleCompositionEnd"
+    @focus="emit('focus')"
+    @blur="emit('blur')"
+  />
+</template>
+
+<style lang="scss" scoped>
+.input-area {
+  width: 100%;
+  font-size: 14px;
+  line-height: 21px;
+  padding: 4px;
+  resize: none;
+  overflow-y: hidden; // 默认不显示滚动条
+  height: 30px; // 初始高度(1 行)
+  max-height: 92px; // 最多 4 行
+  box-sizing: border-box;
+  border: none;
+  outline-color: transparent;
+  outline: none;
+  background-color: transparent;
+  border-radius: 8px;
+  transition: height 0.1s ease;
+  &::placeholder {
+    color: #b5b9bf;
+    opacity: 1;
+  }
+}
+</style>

+ 46 - 0
src/views/AIRobotChat/src/components/LoadingDots.vue

@@ -0,0 +1,46 @@
+<template>
+  <div class="loading-dots">
+    <span class="dot"></span>
+    <span class="dot"></span>
+    <span class="dot"></span>
+  </div>
+</template>
+
+<style scoped>
+.loading-dots {
+  display: flex;
+  align-items: center;
+  justify-content: flex-start;
+  gap: 6px;
+  height: 24px;
+}
+
+.dot {
+  width: 4px;
+  height: 4px;
+  border-radius: 50%;
+  background-color: var(--color-theme);
+  animation: bounce 1.4s infinite ease-in-out both;
+}
+
+.dot:nth-child(1) {
+  animation-delay: 0s;
+}
+.dot:nth-child(2) {
+  animation-delay: 0.2s;
+}
+.dot:nth-child(3) {
+  animation-delay: 0.4s;
+}
+
+@keyframes bounce {
+  0%,
+  80%,
+  100% {
+    transform: translateY(0);
+  }
+  40% {
+    transform: translateY(-4px);
+  }
+}
+</style>

二進制
src/views/AIRobotChat/src/image/icon_ai_robot36_b@2x.png


二進制
src/views/AIRobotChat/src/image/icon_ai_robot48_b@2x.png


二進制
src/views/AIRobotChat/src/image/icon_faq_b@2x.png


二進制
src/views/AIRobotChat/src/image/icon_loading.png


二進制
src/views/AIRobotChat/src/image/robotBubbleDark.png


二進制
src/views/AIRobotChat/src/image/robotBubbleLight.png


Some files were not shown because too many files changed in this diff