Ver Fonte

Merge branch 'dev' of United_Software/k_online_ui into test

Jack Zhou há 4 meses atrás
pai
commit
c4f3c4867e
100 ficheiros alterados com 3472 adições e 551 exclusões
  1. 2 0
      package.json
  2. 103 0
      src/api/module/AIRobot.ts
  3. BIN
      src/assets/image/done-btn.png
  4. BIN
      src/assets/image/next-btn.png
  5. BIN
      src/assets/image/pervious-btn.png
  6. BIN
      src/assets/image/start-btn.png
  7. 19 10
      src/components/AIRobot/src/AIRobot.vue
  8. 36 2
      src/components/MoreFilters/src/MoreFilters.vue
  9. BIN
      src/components/MoreFilters/src/image/booking-dark-more-filters-guide.png
  10. BIN
      src/components/MoreFilters/src/image/booking-more-filters-guide.png
  11. BIN
      src/components/MoreFilters/src/image/tracking-dark-more-filters-guide.png
  12. BIN
      src/components/MoreFilters/src/image/tracking-more-filters-guide.png
  13. 66 17
      src/components/NotificationMessageCard/src/NotificationMessageCard.vue
  14. 3 3
      src/components/NotificationMessageCard/src/components/EventCard.vue
  15. 8 6
      src/components/ScoringGrade/src/ScoringGrade.vue
  16. 12 4
      src/components/VBox_Dashboard/src/VBox_Dashboard.vue
  17. 1 0
      src/components/VDriverGuide/index.ts
  18. 32 0
      src/components/VDriverGuide/src/VDriverGuide.vue
  19. BIN
      src/components/VDriverGuide/src/img/guide-icon.png
  20. 1 1
      src/components/VLoading/src/VLoading.vue
  21. 98 91
      src/components/VSliderVerification/src/VSliderVerification.vue
  22. 817 0
      src/components/VSliderVerification/src/components/SliderVerification.vue
  23. BIN
      src/components/VSliderVerification/src/image/reset.png
  24. 1 0
      src/components/VTipTooltip/index.ts
  25. 55 0
      src/components/VTipTooltip/src/VTipTooltip.vue
  26. 3 1
      src/main.ts
  27. 226 0
      src/stores/modules/guide.ts
  28. 84 0
      src/styles/driver.scss
  29. 2 2
      src/styles/elementui.scss
  30. 20 4
      src/styles/icons/iconfont.css
  31. 0 0
      src/styles/icons/iconfont.js
  32. 6 0
      src/styles/icons/iconfont.svg
  33. BIN
      src/styles/icons/iconfont.ttf
  34. BIN
      src/styles/icons/iconfont.woff
  35. BIN
      src/styles/icons/iconfont.woff2
  36. 6 8
      src/styles/index.scss
  37. 64 12
      src/styles/reset.scss
  38. 2 1
      src/styles/theme-g.scss
  39. 23 0
      src/styles/theme.scss
  40. 2 0
      src/utils/axios.ts
  41. 62 0
      src/utils/driverGuide.ts
  42. 29 23
      src/views/AIApiLog/src/AIApiLog.vue
  43. 39 29
      src/views/AIApiLog/src/components/LogDialog.vue
  44. 43 29
      src/views/AIApiLog/src/components/TableView/src/TableView.vue
  45. 7 1
      src/views/AIApiLog/src/components/TableView/src/components/DownloadDialog.vue
  46. 195 59
      src/views/AIRobotChat/src/AIRobotChat.vue
  47. 10 2
      src/views/AIRobotChat/src/components/AIQuestions.vue
  48. 16 3
      src/views/AIRobotChat/src/components/AutoResizeTextarea.vue
  49. 115 50
      src/views/Booking/src/BookingView.vue
  50. 243 0
      src/views/Booking/src/components/BookingGuide.vue
  51. 18 4
      src/views/Booking/src/components/BookingTable/src/BookingTable.vue
  52. BIN
      src/views/Booking/src/image/customize-columns.png
  53. BIN
      src/views/Booking/src/image/dark-customize-columns.png
  54. 53 45
      src/views/ChatLog/src/ChatLog.vue
  55. 64 17
      src/views/ChatLog/src/components/TableView/src/TableView.vue
  56. 7 1
      src/views/ChatLog/src/components/TableView/src/components/DownloadDialog.vue
  57. 251 21
      src/views/Dashboard/src/DashboardView.vue
  58. 36 4
      src/views/Dashboard/src/components/DashFiters.vue
  59. 156 0
      src/views/Dashboard/src/components/DashboardGuide.vue
  60. BIN
      src/views/Dashboard/src/guideImage/co2e-chart-tip.png
  61. BIN
      src/views/Dashboard/src/guideImage/container-count-chart-tip.png
  62. BIN
      src/views/Dashboard/src/guideImage/dark-kpi-chart-guide.png
  63. BIN
      src/views/Dashboard/src/guideImage/dark-save-config-guide.png
  64. BIN
      src/views/Dashboard/src/guideImage/dark-transport-mode.png
  65. BIN
      src/views/Dashboard/src/guideImage/dark-view-management.png
  66. BIN
      src/views/Dashboard/src/guideImage/etd-to-eta-chart-tip.png
  67. BIN
      src/views/Dashboard/src/guideImage/kpi-chart-guide.png
  68. BIN
      src/views/Dashboard/src/guideImage/kpi-chart-tip.png
  69. BIN
      src/views/Dashboard/src/guideImage/pending-chart-tip.png
  70. BIN
      src/views/Dashboard/src/guideImage/recent-status-chart-tip.png
  71. BIN
      src/views/Dashboard/src/guideImage/revenue-spent-chart-tip.png
  72. BIN
      src/views/Dashboard/src/guideImage/save-config-guide.png
  73. BIN
      src/views/Dashboard/src/guideImage/top-10-chart-tip.png
  74. BIN
      src/views/Dashboard/src/guideImage/transport-mode.png
  75. BIN
      src/views/Dashboard/src/guideImage/view-management.png
  76. BIN
      src/views/Dashboard/src/tipsImage/co2e-chart-tip.png
  77. BIN
      src/views/Dashboard/src/tipsImage/container-count-chart-tip.png
  78. BIN
      src/views/Dashboard/src/tipsImage/dark-co2e-chart-tip.png
  79. BIN
      src/views/Dashboard/src/tipsImage/dark-container-count-chart-tip.png
  80. BIN
      src/views/Dashboard/src/tipsImage/dark-etd-to-eta-chart-tip.png
  81. BIN
      src/views/Dashboard/src/tipsImage/dark-kpi-chart-tip.png
  82. BIN
      src/views/Dashboard/src/tipsImage/dark-pending-chart-tip.png
  83. BIN
      src/views/Dashboard/src/tipsImage/dark-recent-status-chart-tip.png
  84. BIN
      src/views/Dashboard/src/tipsImage/dark-revenue-spent-chart-tip.png
  85. BIN
      src/views/Dashboard/src/tipsImage/dark-top-10-chart-tip.png
  86. BIN
      src/views/Dashboard/src/tipsImage/etd-to-eta-chart-tip.png
  87. BIN
      src/views/Dashboard/src/tipsImage/kpi-chart-tip.png
  88. BIN
      src/views/Dashboard/src/tipsImage/pending-chart-tip.png
  89. BIN
      src/views/Dashboard/src/tipsImage/recent-status-chart-tip.png
  90. BIN
      src/views/Dashboard/src/tipsImage/revenue-spent-chart-tip.png
  91. BIN
      src/views/Dashboard/src/tipsImage/top-10-chart-tip.png
  92. 17 1
      src/views/Layout/src/LayoutView.vue
  93. 36 6
      src/views/Layout/src/components/Header/HeaderView.vue
  94. 23 7
      src/views/Layout/src/components/Header/components/NotificationDrawer.vue
  95. 2 2
      src/views/Layout/src/components/Menu/MenuView.vue
  96. 17 2
      src/views/Login/src/loginView.vue
  97. 98 24
      src/views/SystemMessage/src/SystemMessage.vue
  98. 72 8
      src/views/SystemMessage/src/components/SystemMessageDetail.vue
  99. 87 51
      src/views/Tracking/src/TrackingView.vue
  100. 84 0
      src/views/Tracking/src/components/MultiHighlightGuide.vue

+ 2 - 0
package.json

@@ -27,6 +27,7 @@
     "crypto-js": "^4.2.0",
     "dayjs": "^1.11.13",
     "decimal.js": "^10.4.3",
+    "driver.js": "^1.3.6",
     "echarts": "^5.5.1",
     "element-plus": "^2.8.1",
     "exceljs": "^4.4.0",
@@ -42,6 +43,7 @@
     "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",

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

@@ -133,3 +133,106 @@ export const AIRobotInit = (params: any, config: any) => {
     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
+  )
+}

BIN
src/assets/image/done-btn.png


BIN
src/assets/image/next-btn.png


BIN
src/assets/image/pervious-btn.png


BIN
src/assets/image/start-btn.png


+ 19 - 10
src/components/AIRobot/src/AIRobot.vue

@@ -9,12 +9,12 @@ const AIRobotHoverVisible = ref(false)
 const clicked = ref(false)
 const isShowDefault = ref(false)
 const isShowAIRobotTop = ref(false)
-const AIIconVisible = ref(true)
+const AIIconVisible = ref(false)
 const DeQuestions = ref([])
 const itemGroups = ref([])
 
 const AIRobotInit = () => {
-  $api.AIRobotInit({}).then((res:any) => {
+  $api.AIRobotInit({}).then((res: any) => {
     DeQuestions.value = res.data.fixed_question
     prepareGroups()
   })
@@ -34,9 +34,12 @@ const AvatarMouseLeave = () => {
     AIRobotHoverVisible.value = false
   }
 }
-const emit = defineEmits(['AvatarClick', 'handelClickAIDefault'])
+const emit = defineEmits(['AvatarClick', 'handelClickAIDefault','handelclickaiinit'])
 // 点击AIRobot图标
 const AvatarClick = () => {
+  if(clicked.value == false) {
+    emit('handelclickaiinit')
+  }
   clicked.value = true
   AIRobotHoverVisible.value = false
   isShowDefault.value = false
@@ -59,7 +62,6 @@ const prepareGroups = () => {
   const groups = []
   let currentGroup = []
   let currentHeight = 0
-
   DeQuestions.value.forEach((item) => {
     const itemHeight = item.isLong ? 2 : 1
 
@@ -81,13 +83,14 @@ const prepareGroups = () => {
   itemGroups.value = groups
 }
 const isShowLogin = () => {
-  const LoginDays = 0
+  AIRobotInit()
+  const loginCount = JSON.parse(localStorage.getItem('userInfo')).loginCount
   let settimeouttime = 0
   AIIconVisible.value = true
   isShowDefault.value = true
-  if (LoginDays == 0) {
+  if (loginCount <= 0) {
     settimeouttime = 45000
-  } else if (LoginDays == 2) {
+  } else if (loginCount <= 2) {
     settimeouttime = 15000
   } else {
     settimeouttime = 10000
@@ -104,10 +107,12 @@ const Logout = () => {
   AIRobotHoverVisible.value = false
   isShowDefault.value = false
   isShowAIRobotTop.value = false
+  clicked.value = false
 }
 
 // 退出登录后隐藏icon
 const checknoPrompt = () => {
+  AIRobotInit()
   AIIconVisible.value = true
 }
 
@@ -121,6 +126,9 @@ const handelClick = (item: any) => {
 }
 
 onMounted(() => {
+  if(localStorage.getItem('userInfo') != null) {
+    AIIconVisible.value = true
+  }
   AIRobotInit()
   emitter.on('login-success', isShowLogin)
   emitter.on('login-out', Logout)
@@ -151,7 +159,7 @@ defineExpose({
       <div class="dialogue_title">Hi! I'm your Freight Assistant, always on call</div>
     </div>
     <div class="flex_end">
-      <div class="dialogue_content">
+      <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" />
@@ -230,7 +238,7 @@ defineExpose({
 }
 .AIRobot-top {
   position: absolute;
-  z-index: 2013;
+  z-index: 1999;
   right: 35px;
   bottom: 188px;
 }
@@ -272,7 +280,7 @@ defineExpose({
   background: var(--color-dialogue-icon-bg);
   position: absolute;
   box-shadow: -2px 2px 12px rgba(0, 0, 0, 15%);
-  z-index: 2013;
+  z-index: 1999;
   right: 10px;
   bottom: 130px;
   img {
@@ -288,6 +296,7 @@ defineExpose({
 }
 .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;

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

@@ -527,7 +527,9 @@ const drawer = ref(false)
 
 const props = defineProps({
   isShipment: Boolean,
-  searchTableQeury: Object
+  searchTableQeury: Object,
+  isShowMoreFiltersGuidePhoto: Boolean,
+  pageMode: String
 })
 const PartyTypeoptions = computed(() => {
   if (props.isShipment) {
@@ -1007,9 +1009,33 @@ watch(
     searchTableQeurytest.value = current
   }
 )
+import trackingMoreFiltersImgLight from './image/tracking-more-filters-guide.png'
+import trackingMoreFiltersImgDark from './image/tracking-dark-more-filters-guide.png'
+import bookingMoreFiltersImgLight from './image/booking-more-filters-guide.png'
+import bookingMoreFiltersImgDark from './image/booking-dark-more-filters-guide.png'
+import { useThemeStore } from '@/stores/modules/theme'
+
+const themeStore = useThemeStore()
+
+const moreFiltersGuideImg = computed(() => {
+  if (props.pageMode === 'tracking') {
+    return themeStore.theme === 'dark' ? trackingMoreFiltersImgDark : trackingMoreFiltersImgLight
+  } else {
+    return themeStore.theme === 'dark' ? bookingMoreFiltersImgDark : bookingMoreFiltersImgLight
+  }
+})
 </script>
 <template>
-  <div>
+  <div style="position: relative">
+    <div style="width: 0; height: 0">
+      <img
+        id="more-filters-guide"
+        v-show="props.isShowMoreFiltersGuidePhoto"
+        class="more-filters-guide-class position-absolute-guide"
+        :src="moreFiltersGuideImg"
+        alt=""
+      />
+    </div>
     <el-button class="More_Filters el-button--grey" @click="clickmorefilters">
       <span class="iconfont_icon icon_more">
         <svg class="iconfont" aria-hidden="true">
@@ -1211,6 +1237,13 @@ watch(
 </template>
 
 <style lang="scss" scoped>
+img.more-filters-guide-class {
+  right: -20px;
+  top: -1px;
+  width: 361px;
+  z-index: 1500;
+}
+
 .icon_more {
   margin-left: 8px;
   margin-right: 0;
@@ -1242,6 +1275,7 @@ watch(
 
 .Filters_title {
   margin: 0 8px;
+  margin-left: 7px;
 }
 
 :deep(.el-drawer__header) {

BIN
src/components/MoreFilters/src/image/booking-dark-more-filters-guide.png


BIN
src/components/MoreFilters/src/image/booking-more-filters-guide.png


BIN
src/components/MoreFilters/src/image/tracking-dark-more-filters-guide.png


BIN
src/components/MoreFilters/src/image/tracking-more-filters-guide.png


+ 66 - 17
src/components/NotificationMessageCard/src/NotificationMessageCard.vue

@@ -4,7 +4,7 @@ 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 { DynamicScroller, DynamicScrollerItem } from 'vue3-virtual-scroller'
 import 'vue3-virtual-scroller/dist/vue3-virtual-scroller.css'
 import { formatTimezone } from '@/utils/tools'
 
@@ -27,7 +27,13 @@ const props = withDefaults(
 )
 const pageData = ref<any[]>([])
 
-const emit = defineEmits<{ seeAll: []; hasCardRead: []; viewMore: []; jumpTracking: [] }>()
+const emit = defineEmits<{
+  seeAll: []
+  hasCardRead: []
+  viewMore: []
+  jumpTracking: []
+  loading: []
+}>()
 const handleSeeAll = () => {
   emit('seeAll')
 }
@@ -166,9 +172,7 @@ const setAllMessageRead = () => {
     item.info.isRead = true
   })
 }
-defineExpose({
-  setAllMessageRead
-})
+
 // 定时将消息卡片未读的置为已读,五分钟一次
 let timer = null
 onMounted(() => {
@@ -195,21 +199,53 @@ const parentHeight = computed(() => {
 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>
-  <DynamicScroller
-    class="scroller"
-    :style="scrollParentBoxStyle"
-    :items="pageData"
-    :min-item-size="100"
-    key-field="id"
-  >
-    <template v-slot="{ item, index, active }">
-      <DynamicScrollerItem
+  <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"
-        :active="active"
         :size-dependencies="[item.info.isRead]"
       >
         <div
@@ -234,9 +270,18 @@ const scrollParentBoxStyle = computed(() => {
             {{ formatTimezone(item.info.first_notifiation_date) }}
           </div>
         </div>
-      </DynamicScrollerItem>
+      </div>
     </template>
-  </DynamicScroller>
+    <div
+      class="footer"
+      v-if="pageData?.[0]?.notificationType !== 'feature' && pageData?.length > 0"
+    >
+      <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>
@@ -246,6 +291,10 @@ const scrollParentBoxStyle = computed(() => {
   .scroll-padding {
     padding: 0px 140px 0px 16px;
   }
+  .footer {
+    padding: 0 16px;
+    text-align: center;
+  }
 }
 .notification-message-card {
   position: relative;

+ 3 - 3
src/components/NotificationMessageCard/src/components/EventCard.vue

@@ -161,7 +161,7 @@ const jumpTracking = (data: EventCardPropsData) => {
           dayjs(data.info.time).format('MMM DD, YYYY HH:mm')
         }}</span>
         <span>{{ getTimezone(data.info.timezone, data.info.time) }}</span>
-        <span>&nbsp;({{ data.info.delayTimeTip }})</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>
@@ -346,8 +346,8 @@ const jumpTracking = (data: EventCardPropsData) => {
     }
     .previous {
       margin-top: 8px;
-      padding-left: 8px;
-      line-height: 24px;
+      padding: 4px 8px;
+      line-height: 16px;
       background-color: var(--color-previous-bg);
       border-radius: 6px;
       span {

+ 8 - 6
src/components/ScoringGrade/src/ScoringGrade.vue

@@ -299,7 +299,7 @@ onMounted(() => {
       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
@@ -412,16 +412,18 @@ div.scoring {
   background-color: var(--management-bg-color);
   position: absolute;
   box-shadow: -2px 2px 12px rgba(0, 0, 0, 15%);
-  z-index: 2013;
+  z-index: 1999;
   right: 10px;
   bottom: 64px;
   display: flex;
   align-items: center;
   justify-content: center;
-  img {
-    width: 36px;
-    height: 36px;
-    margin: 3px 0 0 4px;
+  .avatar_bg {
+    img {
+      width: 36px;
+      height: 36px;
+      margin: 3px 0 0 4px;
+    }
   }
 }
 .el-avatar {

+ 12 - 4
src/components/VBox_Dashboard/src/VBox_Dashboard.vue

@@ -3,6 +3,7 @@ import { ref } from 'vue'
 const props = withDefaults(
   defineProps<{
     id?: number
+    isShowDragIconGudie?: boolean
   }>(),
   {}
 )
@@ -44,13 +45,20 @@ const vBoxPopoverRef = ref()
   <div class="v-box">
     <div class="header">
       <slot name="header">Title</slot>
-      <div class="option">
-        <el-button type="text" class="sort handle-draggable">
-          <span class="iconfont_icon">
+      <div class="option" style="width: 48px; height: 48px">
+        <el-button
+          type="text"
+          class="sort handle-draggable"
+          :id="isShowDragIconGudie ? 'drag-icon-guide' : ''"
+        >
+          <!-- <span class="iconfont_icon">
             <svg class="iconfont" aria-hidden="true">
               <use xlink:href="#icon-icon_dragsort__b"></use>
             </svg>
-          </span>
+          </span> -->
+
+          <!--  -->
+          <span class="font_family icon-icon_dragsort__b"></span>
         </el-button>
       </div>
       <div class="cancel" @click="changeCancel">

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

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

+ 32 - 0
src/components/VDriverGuide/src/VDriverGuide.vue

@@ -0,0 +1,32 @@
+<script setup lang="ts"></script>
+
+<template>
+  <div id="page-guide-btn-guide" class="driver-guide-btn">
+    <img src="./img/guide-icon.png" alt="" />
+    <span style="margin-top: 2px; font-size: 12px; font-style: italic; color: var(--color-theme)">
+      Page Guide
+    </span>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.driver-guide-btn {
+  display: inline-flex;
+  align-items: center;
+  justify-content: center;
+  height: 24px;
+  padding: 0 7px;
+  margin-left: 8px;
+  border-radius: 24px;
+  background: var(--color-guide-icon-bg);
+  cursor: pointer;
+  &:hover {
+    background: var(--color-guide-icon-hover-bg);
+  }
+  img {
+    width: 16px;
+    height: 16px;
+    margin-right: 2px;
+  }
+}
+</style>

BIN
src/components/VDriverGuide/src/img/guide-icon.png


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

+ 98 - 91
src/components/VSliderVerification/src/VSliderVerification.vue

@@ -6,7 +6,8 @@ import Img03 from './image/verification-img-3.png'
 import Img04 from './image/verification-img-4.png'
 import { ref } from 'vue'
 //引入'vue3-puzzle-vcode'插件
-import Vcode from 'vue3-puzzle-vcode'
+import Vcode from './components/SliderVerification.vue'
+// import Vcode from 'vue3-puzzle-vcode'
 
 const openDialog = () => {
   isShow.value = true
@@ -31,10 +32,13 @@ const updateRefreshImg = () => {
   }
 }
 const addTipsNode = () => {
+  if (document.querySelector('.v-slider-verification-tips')) {
+    return
+  }
   const childNode = document.createElement('div')
-  childNode.className = 'tips'
+  childNode.className = 'v-slider-verification-tips'
   childNode.innerHTML = `
-    <div style="margin-bottom: 15px; text-align: right;">
+    <div style="margin-bottom: 5px; text-align: right;">
       <span class="font_family icon-icon_reject_b close-icon" style="margin-right: -20px;">
       </span>
     </div>
@@ -54,6 +58,7 @@ const addTipsNode = () => {
 }
 
 const closeDialog = () => {
+  isShow.value = false
   emit('close')
 }
 
@@ -131,6 +136,7 @@ const onSuccess = () => {
 }
 const close = () => {
   isShow.value = true
+  emit('close')
 }
 const fail = () => {
   updateSliderBackground('error')
@@ -146,113 +152,114 @@ defineExpose({
 })
 </script>
 <template>
-  <Vcode
-    :show="isShow"
-    @close="close"
-    @fail="fail"
-    :canvasWidth="320"
-    :canvasHeight="180"
-    sliderText="Swipe to verify"
-    :sliderSize="38"
-    successText="Verification successful"
-    failText="Verification failed"
-    :range="5"
-    :imgs="[Img01, Img02, Img03, Img04]"
-  ></Vcode>
+  <div>
+    <Vcode
+      :show="isShow"
+      @close="close"
+      @fail="fail"
+      :canvasWidth="320"
+      :canvasHeight="180"
+      sliderText="Swipe to verify"
+      :sliderSize="38"
+      successText="Verification successful"
+      failText="Verification failed"
+      :range="5"
+      :imgs="[Img01, Img02, Img03, Img04]"
+    ></Vcode>
+  </div>
 </template>
 <style lang="scss" scoped>
 .slider-verification {
   width: 400px;
-  height: 373px;
+  height: 365px;
   padding: 40px;
-  .tips {
-    text-align: center;
-  }
 }
 </style>
 <style lang="scss">
-// 整体框架
-.vue-auth-box_ {
-  width: 400px;
-  height: 373px;
-  padding: 40px;
-  padding-top: 20px;
-  background-color: var(--color-slider-bg);
-  border-radius: 16px;
-  box-shadow: -2px 2px 12px 0 rgba(0, 0, 0, 0.5);
-  .tips {
-    margin-bottom: 16px;
-    text-align: center;
-    .close-icon {
-      cursor: pointer;
-      &:hover {
-        color: var(--color-theme);
+.vue-puzzle-vcode {
+  // 整体框架
+  div.vue-auth-box_ {
+    width: 400px;
+    height: 365px;
+    padding: 40px;
+    padding-top: 16px;
+    background-color: var(--color-slider-bg);
+    border-radius: 16px;
+    box-shadow: -2px 2px 12px 0 rgba(0, 0, 0, 0.5);
+    .v-slider-verification-tips {
+      margin-bottom: 16px;
+      text-align: center;
+      .close-icon {
+        cursor: pointer;
+        &:hover {
+          color: var(--color-theme);
+        }
       }
     }
-  }
-  .icon-border {
-    display: flex;
-    justify-content: center;
-    align-items: center;
-    width: 16px !important;
-    height: 16px !important;
-    border-radius: 50%;
-    border: none !important;
-    span {
-      font-size: 14px !important;
+    .icon-border {
+      display: flex;
+      justify-content: center;
+      align-items: center;
+      width: 16px !important;
+      height: 16px !important;
+      border-radius: 50%;
+      border: none !important;
+      span {
+        font-size: 14px !important;
+      }
     }
   }
-}
 
-div.vue-puzzle-vcode {
-  background-color: rgba(43, 47, 54, 0.7);
-}
+  div.vue-puzzle-vcode {
+    background-color: rgba(43, 47, 54, 0.7);
+  }
 
-.vue-auth-box_ .auth-control_ div.range-box {
-  background-color: #87909e;
-  box-shadow: none;
-}
-.vue-auth-box_ div.auth-body_ {
-  border-radius: 6px;
-}
-// 已滑动区域的样式
-.vue-auth-box_ .auth-control_ .range-box div.range-slider {
-  background-color: var(--color-success);
-}
+  .vue-auth-box_ .auth-control_ div.range-box {
+    background-color: #87909e;
+    box-shadow: none;
+  }
+  .vue-auth-box_ div.auth-body_ {
+    border-radius: 6px;
+  }
+  // 已滑动区域的样式
+  .vue-auth-box_ .auth-control_ .range-box div.range-slider {
+    background-color: var(--color-success);
+  }
 
-.vue-auth-box_ .auth-body_ div.loading-box_.hide_ {
-  display: none;
-}
+  .vue-auth-box_ .auth-body_ div.loading-box_.hide_ {
+    display: none;
+  }
 
-// 成功的提示
-.vue-auth-box_ .auth-body_ div.info-box_ {
-  background: var(--color-success);
-}
-// 失败的提示
-.vue-auth-box_ .auth-body_ div.info-box_.fail {
-  background: #c7353f;
-}
+  // 成功的提示
+  .vue-auth-box_ .auth-body_ div.info-box_ {
+    background: var(--color-success);
+  }
+  // 失败的提示
+  .vue-auth-box_ .auth-body_ div.info-box_.fail {
+    background: #c7353f;
+  }
 
-.vue-auth-box_ .auth-control_ .range-box .range-slider {
-  border-radius: 6px;
-}
+  .vue-auth-box_ .auth-control_ .range-box .range-slider {
+    border-radius: 6px;
+  }
 
-.vue-auth-box_ .auth-control_ div.range-box {
-  border-radius: 6px;
-}
-// 滑块
-.vue-auth-box_ .auth-control_ .range-box .range-slider .range-btn {
-  border-radius: 6px;
-}
+  .vue-auth-box_ .auth-control_ div.range-box {
+    border-radius: 6px;
+  }
+  // 滑块
+  .vue-auth-box_ .auth-control_ .range-box .range-slider .range-btn {
+    border-radius: 6px;
+  }
 
-.vue-auth-box_ .auth-body_ .reset_ {
-  width: 20px;
-  height: 20px;
-  top: 10px;
-  right: 10px;
-}
+  .vue-auth-box_ .auth-body_ .reset_ {
+    width: 20px;
+    height: 20px;
+    top: 10px;
+    right: 10px;
+  }
 
-.vue-auth-box_ .auth-control_ .range-box .range-text {
-  color: #fff;
+  .vue-auth-box_ .auth-control_ .range-box .range-text {
+    color: #fff;
+  }
 }
 </style>

+ 817 - 0
src/components/VSliderVerification/src/components/SliderVerification.vue

@@ -0,0 +1,817 @@
+<template>
+  <!-- 本体部分 -->
+  <div
+    :class="['vue-puzzle-vcode', { show_: show }]"
+    @mousedown="onCloseMouseDown"
+    @mouseup="onCloseMouseUp"
+    @touchstart="onCloseMouseDown"
+    @touchend="onCloseMouseUp"
+  >
+    <div class="vue-auth-box_" @mousedown.stop @touchstart.stop>
+      <div class="auth-body_" :style="`height: ${canvasHeight}px`">
+        <!-- 主图,有缺口 -->
+        <canvas
+          ref="canvas1"
+          :width="canvasWidth"
+          :height="canvasHeight"
+          :style="`width:${canvasWidth}px;height:${canvasHeight}px`"
+        />
+        <!-- 成功后显示的完整图 -->
+        <canvas
+          ref="canvas3"
+          :class="['auth-canvas3_', { show: isSuccess }]"
+          :width="canvasWidth"
+          :height="canvasHeight"
+          :style="`width:${canvasWidth}px;height:${canvasHeight}px`"
+        />
+        <!-- 小图 -->
+        <canvas
+          :width="puzzleBaseSize"
+          class="auth-canvas2_"
+          :height="canvasHeight"
+          ref="canvas2"
+          @mousedown="onRangeMouseDown($event)"
+          @touchstart="onRangeMouseDown($event)"
+          :style="`width:${puzzleBaseSize}px;height:${canvasHeight}px;transform:translateX(${
+            styleWidth -
+            sliderBaseSize -
+            (puzzleBaseSize - sliderBaseSize) *
+              ((styleWidth - sliderBaseSize) / (canvasWidth - sliderBaseSize))
+          }px)`"
+        />
+        <div :class="['loading-box_', { hide_: !loading }]">
+          <div class="loading-gif_">
+            <span></span>
+            <span></span>
+            <span></span>
+            <span></span>
+            <span></span>
+          </div>
+        </div>
+        <div :class="['info-box_', { show: infoBoxShow }, { fail: infoBoxFail }]">
+          {{ infoText }}
+        </div>
+        <div
+          :class="['flash_', { show: isSuccess }]"
+          :style="`transform: translateX(${
+            isSuccess ? `${canvasWidth + canvasHeight * 0.578}px` : `-${canvasHeight * 0.578}px`
+          }) skew(-30deg, 0);`"
+        ></div>
+        <img class="reset_" @click="reset" :src="resetSvg" />
+      </div>
+      <div class="auth-control_">
+        <div class="range-box" :style="`height:${sliderBaseSize}px`">
+          <div class="range-text">{{ sliderText }}</div>
+          <div class="range-slider" ref="range-slider" :style="`width:${styleWidth}px`">
+            <div
+              :class="['range-btn', { isDown: mouseDown }]"
+              :style="`width:${sliderBaseSize}px`"
+              @mousedown="onRangeMouseDown($event)"
+              @touchstart="onRangeMouseDown($event)"
+            >
+              <div></div>
+              <div></div>
+              <div></div>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+<script>
+import resetSvg from '../image/reset.png'
+export default {
+  props: {
+    canvasWidth: { type: Number, default: 310 }, // 主canvas的宽
+    canvasHeight: { type: Number, default: 160 }, // 主canvas的高
+    // 是否出现,由父级控制
+    show: { type: Boolean, default: false },
+    puzzleScale: { type: Number, default: 1 }, // 拼图块的大小缩放比例
+    sliderSize: { type: Number, default: 50 }, // 滑块的大小
+    range: { type: Number, default: 10 }, // 允许的偏差值
+    // 所有的背景图片
+    imgs: {
+      type: Array
+    },
+    successText: {
+      type: String,
+      default: '验证通过!'
+    },
+    failText: {
+      type: String,
+      default: '验证失败,请重试'
+    },
+    sliderText: {
+      type: String,
+      default: '拖动滑块完成拼图'
+    }
+  },
+
+  data() {
+    return {
+      mouseDown: false, // 鼠标是否在按钮上按下
+      startWidth: 50, // 鼠标点下去时父级的width
+      startX: 0, // 鼠标按下时的X
+      newX: 0, // 鼠标当前的偏移X
+      pinX: 0, // 拼图的起始X
+      pinY: 0, // 拼图的起始Y
+      loading: false, // 是否正在加在中,主要是等图片onload
+      isCanSlide: false, // 是否可以拉动滑动条
+      error: false, // 图片加在失败会出现这个,提示用户手动刷新
+      infoBoxShow: false, // 提示信息是否出现
+      infoText: '', // 提示等信息
+      infoBoxFail: false, // 是否验证失败
+      timer1: null, // setTimout1
+      closeDown: false, // 为了解决Mac上的click BUG
+      isSuccess: false, // 验证成功
+      imgIndex: -1, // 用于自定义图片时不会随机到重复的图片
+      isSubmting: false, // 是否正在判定,主要用于判定中不能点击重置按钮
+      resetSvg
+    }
+  },
+
+  /** 生命周期 **/
+  mounted() {
+    document.body.appendChild(this.$el)
+    document.addEventListener('mousemove', this.onRangeMouseMove, false)
+    document.addEventListener('mouseup', this.onRangeMouseUp, false)
+
+    document.addEventListener('touchmove', this.onRangeMouseMove, {
+      passive: false
+    })
+    document.addEventListener('touchend', this.onRangeMouseUp, false)
+    if (this.show) {
+      document.body.classList.add('vue-puzzle-overflow')
+      this.reset()
+    }
+  },
+  beforeUnmount() {
+    clearTimeout(this.timer1)
+    document.body.removeChild(this.$el)
+    document.removeEventListener('mousemove', this.onRangeMouseMove, false)
+    document.removeEventListener('mouseup', this.onRangeMouseUp, false)
+
+    document.removeEventListener('touchmove', this.onRangeMouseMove, {
+      passive: false
+    })
+    document.removeEventListener('touchend', this.onRangeMouseUp, false)
+  },
+
+  /** 监听 **/
+  watch: {
+    show(newV) {
+      // 每次出现都应该重新初始化
+      if (newV) {
+        document.body.classList.add('vue-puzzle-overflow')
+        this.reset()
+      } else {
+        this.isSubmting = false
+        this.isSuccess = false
+        this.infoBoxShow = false
+        document.body.classList.remove('vue-puzzle-overflow')
+      }
+    }
+  },
+
+  /** 计算属性 **/
+  computed: {
+    // styleWidth是底部用户操作的滑块的父级,就是轨道在鼠标的作用下应该具有的宽度
+    styleWidth() {
+      const w = this.startWidth + this.newX - this.startX
+      return w < this.sliderBaseSize
+        ? this.sliderBaseSize
+        : w > this.canvasWidth
+          ? this.canvasWidth
+          : w
+    },
+    // 图中拼图块的60 * 用户设定的缩放比例计算之后的值 0.2~2
+    puzzleBaseSize() {
+      return Math.round(Math.max(Math.min(this.puzzleScale, 2), 0.2) * 52.5 + 6)
+    },
+    // 处理一下sliderSize,弄成整数,以免计算有偏差
+    sliderBaseSize() {
+      return Math.max(Math.min(Math.round(this.sliderSize), Math.round(this.canvasWidth * 0.5)), 10)
+    }
+  },
+
+  /** 方法 **/
+  methods: {
+    // 关闭
+    onClose() {
+      if (!this.mouseDown && !this.isSubmting) {
+        clearTimeout(this.timer1)
+        this.$emit('close')
+      }
+    },
+    onCloseMouseDown() {
+      this.closeDown = true
+    },
+    onCloseMouseUp() {
+      if (this.closeDown) {
+        this.onClose()
+      }
+      this.closeDown = false
+    },
+    // 鼠标按下准备拖动
+    onRangeMouseDown(e) {
+      if (this.isCanSlide) {
+        this.mouseDown = true
+        this.startWidth = this.$refs['range-slider'].clientWidth
+        this.newX = e.clientX || e.changedTouches[0].clientX
+        this.startX = e.clientX || e.changedTouches[0].clientX
+      }
+    },
+    // 鼠标移动
+    onRangeMouseMove(e) {
+      if (this.mouseDown) {
+        e.preventDefault()
+        this.newX = e.clientX || e.changedTouches[0].clientX
+      }
+    },
+    // 鼠标抬起
+    onRangeMouseUp() {
+      if (this.mouseDown) {
+        this.mouseDown = false
+        this.submit()
+      }
+    },
+    /**
+     * 开始进行
+     * @param withCanvas 是否强制使用canvas随机作图
+     */
+    init(withCanvas) {
+      // 防止重复加载导致的渲染错误
+      if (this.loading && !withCanvas) {
+        return
+      }
+      this.loading = true
+      this.isCanSlide = false
+      const c = this.$refs.canvas1
+      const c2 = this.$refs.canvas2
+      const c3 = this.$refs.canvas3
+      const ctx = c.getContext('2d')
+      const ctx2 = c2.getContext('2d')
+      const ctx3 = c3.getContext('2d')
+      const isFirefox =
+        navigator.userAgent.indexOf('Firefox') >= 0 && navigator.userAgent.indexOf('Windows') >= 0 // 是windows版火狐
+      const img = document.createElement('img')
+      ctx.fillStyle = 'rgba(255,255,255,1)'
+      ctx3.fillStyle = 'rgba(255,255,255,1)'
+      ctx.clearRect(0, 0, this.canvasWidth, this.canvasHeight)
+      ctx2.clearRect(0, 0, this.canvasWidth, this.canvasHeight)
+
+      // 取一个随机坐标,作为拼图块的位置
+      this.pinX = this.getRandom(this.puzzleBaseSize, this.canvasWidth - this.puzzleBaseSize - 20) // 留20的边距
+      this.pinY = this.getRandom(20, this.canvasHeight - this.puzzleBaseSize - 20) // 主图高度 - 拼图块自身高度 - 20边距
+      img.crossOrigin = 'anonymous' // 匿名,想要获取跨域的图片
+      img.onload = () => {
+        const [x, y, w, h] = this.makeImgSize(img)
+        ctx.save()
+        // 先画小图
+        this.paintBrick(ctx)
+        ctx.closePath()
+        if (!isFirefox) {
+          ctx.shadowOffsetX = 0
+          ctx.shadowOffsetY = 0
+          ctx.shadowColor = '#000'
+          ctx.shadowBlur = 3
+          ctx.fill()
+          ctx.clip()
+        } else {
+          ctx.clip()
+          ctx.save()
+          ctx.shadowOffsetX = 0
+          ctx.shadowOffsetY = 0
+          ctx.shadowColor = '#000'
+          ctx.shadowBlur = 3
+          ctx.fill()
+          ctx.restore()
+        }
+
+        ctx.drawImage(img, x, y, w, h)
+        ctx3.fillRect(0, 0, this.canvasWidth, this.canvasHeight)
+        ctx3.drawImage(img, x, y, w, h)
+
+        // 设置小图的内阴影
+        ctx.globalCompositeOperation = 'source-atop'
+
+        this.paintBrick(ctx)
+
+        ctx.arc(
+          this.pinX + Math.ceil(this.puzzleBaseSize / 2),
+          this.pinY + Math.ceil(this.puzzleBaseSize / 2),
+          this.puzzleBaseSize * 1.2,
+          0,
+          Math.PI * 2,
+          true
+        )
+        ctx.closePath()
+        ctx.shadowColor = 'rgba(255, 255, 255, .8)'
+        ctx.shadowOffsetX = -1
+        ctx.shadowOffsetY = -1
+        ctx.shadowBlur = Math.min(Math.ceil(8 * this.puzzleScale), 12)
+        ctx.fillStyle = '#ffffaa'
+        ctx.fill()
+
+        // 将小图赋值给ctx2
+        const imgData = ctx.getImageData(
+          this.pinX - 3, // 为了阴影 是从-3px开始截取,判定的时候要+3px
+          this.pinY - 20,
+          this.pinX + this.puzzleBaseSize + 5,
+          this.pinY + this.puzzleBaseSize + 5
+        )
+        ctx2.putImageData(imgData, 0, this.pinY - 20)
+
+        // ctx2.drawImage(c, this.pinX - 3,this.pinY - 20,this.pinX + this.puzzleBaseSize + 5,this.pinY + this.puzzleBaseSize + 5,
+        // 0, this.pinY - 20, this.pinX + this.puzzleBaseSize + 5, this.pinY + this.puzzleBaseSize + 5);
+
+        // 清理
+        ctx.restore()
+        ctx.clearRect(0, 0, this.canvasWidth, this.canvasHeight)
+
+        // 画缺口
+        ctx.save()
+        this.paintBrick(ctx)
+        ctx.globalAlpha = 0.8
+        ctx.fillStyle = '#ffffff'
+        ctx.fill()
+        ctx.restore()
+
+        // 画缺口的内阴影
+        ctx.save()
+        ctx.globalCompositeOperation = 'source-atop'
+        this.paintBrick(ctx)
+        ctx.arc(
+          this.pinX + Math.ceil(this.puzzleBaseSize / 2),
+          this.pinY + Math.ceil(this.puzzleBaseSize / 2),
+          this.puzzleBaseSize * 1.2,
+          0,
+          Math.PI * 2,
+          true
+        )
+        ctx.shadowColor = '#000'
+        ctx.shadowOffsetX = 2
+        ctx.shadowOffsetY = 2
+        ctx.shadowBlur = 16
+        ctx.fill()
+        ctx.restore()
+
+        // 画整体背景图
+        ctx.save()
+        ctx.globalCompositeOperation = 'destination-over'
+        ctx.drawImage(img, x, y, w, h)
+        ctx.restore()
+
+        this.loading = false
+        this.isCanSlide = true
+      }
+      img.onerror = () => {
+        this.init(true) // 如果图片加载错误就重新来,并强制用canvas随机作图
+      }
+
+      if (!withCanvas && this.imgs && this.imgs.length) {
+        let randomNum = this.getRandom(0, this.imgs.length - 1)
+        if (randomNum === this.imgIndex) {
+          if (randomNum === this.imgs.length - 1) {
+            randomNum = 0
+          } else {
+            randomNum++
+          }
+        }
+        this.imgIndex = randomNum
+        img.src = this.imgs[randomNum]
+      } else {
+        img.src = this.makeImgWithCanvas()
+      }
+    },
+    // 工具 - 范围随机数
+    getRandom(min, max) {
+      return Math.ceil(Math.random() * (max - min) + min)
+    },
+    // 工具 - 设置图片尺寸cover方式贴合canvas尺寸 w/h
+    makeImgSize(img) {
+      const imgScale = img.width / img.height
+      const canvasScale = this.canvasWidth / this.canvasHeight
+      let x = 0,
+        y = 0,
+        w = 0,
+        h = 0
+      if (imgScale > canvasScale) {
+        h = this.canvasHeight
+        w = imgScale * h
+        y = 0
+        x = (this.canvasWidth - w) / 2
+      } else {
+        w = this.canvasWidth
+        h = w / imgScale
+        x = 0
+        y = (this.canvasHeight - h) / 2
+      }
+      return [x, y, w, h]
+    },
+    // 绘制拼图块的路径
+    paintBrick(ctx) {
+      const moveL = Math.ceil(15 * this.puzzleScale) // 直线移动的基础距离
+      ctx.beginPath()
+      ctx.moveTo(this.pinX, this.pinY)
+      ctx.lineTo(this.pinX + moveL, this.pinY)
+      ctx.arcTo(
+        this.pinX + moveL,
+        this.pinY - moveL / 2,
+        this.pinX + moveL + moveL / 2,
+        this.pinY - moveL / 2,
+        moveL / 2
+      )
+      ctx.arcTo(
+        this.pinX + moveL + moveL,
+        this.pinY - moveL / 2,
+        this.pinX + moveL + moveL,
+        this.pinY,
+        moveL / 2
+      )
+      ctx.lineTo(this.pinX + moveL + moveL + moveL, this.pinY)
+      ctx.lineTo(this.pinX + moveL + moveL + moveL, this.pinY + moveL)
+      ctx.arcTo(
+        this.pinX + moveL + moveL + moveL + moveL / 2,
+        this.pinY + moveL,
+        this.pinX + moveL + moveL + moveL + moveL / 2,
+        this.pinY + moveL + moveL / 2,
+        moveL / 2
+      )
+      ctx.arcTo(
+        this.pinX + moveL + moveL + moveL + moveL / 2,
+        this.pinY + moveL + moveL,
+        this.pinX + moveL + moveL + moveL,
+        this.pinY + moveL + moveL,
+        moveL / 2
+      )
+      ctx.lineTo(this.pinX + moveL + moveL + moveL, this.pinY + moveL + moveL + moveL)
+      ctx.lineTo(this.pinX, this.pinY + moveL + moveL + moveL)
+      ctx.lineTo(this.pinX, this.pinY + moveL + moveL)
+
+      ctx.arcTo(
+        this.pinX + moveL / 2,
+        this.pinY + moveL + moveL,
+        this.pinX + moveL / 2,
+        this.pinY + moveL + moveL / 2,
+        moveL / 2
+      )
+      ctx.arcTo(this.pinX + moveL / 2, this.pinY + moveL, this.pinX, this.pinY + moveL, moveL / 2)
+      ctx.lineTo(this.pinX, this.pinY)
+    },
+    // 用canvas随机生成图片
+    makeImgWithCanvas() {
+      const canvas = document.createElement('canvas')
+      const ctx = canvas.getContext('2d')
+      canvas.width = this.canvasWidth
+      canvas.height = this.canvasHeight
+      ctx.fillStyle = `rgb(${this.getRandom(100, 255)},${this.getRandom(100, 255)},${this.getRandom(
+        100,
+        255
+      )})`
+      ctx.fillRect(0, 0, this.canvasWidth, this.canvasHeight)
+      // 随机画10个图形
+      for (let i = 0; i < 12; i++) {
+        ctx.fillStyle = `rgb(${this.getRandom(100, 255)},${this.getRandom(
+          100,
+          255
+        )},${this.getRandom(100, 255)})`
+        ctx.strokeStyle = `rgb(${this.getRandom(100, 255)},${this.getRandom(
+          100,
+          255
+        )},${this.getRandom(100, 255)})`
+
+        if (this.getRandom(0, 2) > 1) {
+          // 矩形
+          ctx.save()
+          ctx.rotate((this.getRandom(-90, 90) * Math.PI) / 180)
+          ctx.fillRect(
+            this.getRandom(-20, canvas.width - 20),
+            this.getRandom(-20, canvas.height - 20),
+            this.getRandom(10, canvas.width / 2 + 10),
+            this.getRandom(10, canvas.height / 2 + 10)
+          )
+          ctx.restore()
+        } else {
+          // 圆
+          ctx.beginPath()
+          const ran = this.getRandom(-Math.PI, Math.PI)
+          ctx.arc(
+            this.getRandom(0, canvas.width),
+            this.getRandom(0, canvas.height),
+            this.getRandom(10, canvas.height / 2 + 10),
+            ran,
+            ran + Math.PI * 1.5
+          )
+          ctx.closePath()
+          ctx.fill()
+        }
+      }
+      return canvas.toDataURL('image/png')
+    },
+    // 开始判定
+    submit() {
+      this.isSubmting = true
+      // 偏差 x = puzzle的起始X - (用户真滑动的距离) + (puzzle的宽度 - 滑块的宽度) * (用户真滑动的距离/canvas总宽度)
+      // 最后+ 的是补上slider和滑块宽度不一致造成的缝隙
+      const x = Math.abs(
+        this.pinX -
+          (this.styleWidth - this.sliderBaseSize) +
+          (this.puzzleBaseSize - this.sliderBaseSize) *
+            ((this.styleWidth - this.sliderBaseSize) / (this.canvasWidth - this.sliderBaseSize)) -
+          3
+      )
+      if (x < this.range) {
+        // 成功
+        this.infoText = this.successText
+        this.infoBoxFail = false
+        this.infoBoxShow = true
+        this.isCanSlide = false
+        this.isSuccess = true
+        // 成功后准备关闭
+        clearTimeout(this.timer1)
+        this.timer1 = setTimeout(() => {
+          // 成功的回调
+          this.isSubmting = false
+          this.$emit('success', x)
+        }, 400)
+      } else {
+        // 失败
+        this.infoText = this.failText
+        this.infoBoxFail = true
+        this.infoBoxShow = true
+        this.isCanSlide = false
+        // 失败的回调
+        this.$emit('fail', x)
+        // 400ms后重置
+        clearTimeout(this.timer1)
+        this.timer1 = setTimeout(() => {
+          this.isSubmting = false
+          this.reset()
+        }, 400)
+      }
+    },
+    // 重置 - 重新设置初始状态
+    resetState() {
+      this.infoBoxFail = false
+      this.infoBoxShow = false
+      this.isCanSlide = false
+      this.isSuccess = false
+      this.startWidth = this.sliderBaseSize // 鼠标点下去时父级的width
+      this.startX = 0 // 鼠标按下时的X
+      this.newX = 0 // 鼠标当前的偏移X
+    },
+
+    // 重置
+    reset() {
+      if (this.isSubmting) {
+        return
+      }
+      this.resetState()
+      this.init()
+    }
+  }
+}
+</script>
+<style lang="scss" scoped>
+.vue-puzzle-vcode {
+  position: fixed;
+  top: 0;
+  left: 0;
+  bottom: 0;
+  right: 0;
+  background-color: rgba(0, 0, 0, 0.3);
+  z-index: 999;
+  opacity: 0;
+  pointer-events: none;
+  transition: opacity 200ms;
+  &.show_ {
+    opacity: 1;
+    pointer-events: auto;
+  }
+}
+.vue-auth-box_ {
+  position: absolute;
+  top: 40%;
+  left: 50%;
+  transform: translate(-50%, -50%);
+  padding: 20px;
+  background: #fff;
+  user-select: none;
+  border-radius: 3px;
+  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.3);
+  .auth-body_ {
+    position: relative;
+    overflow: hidden;
+    border-radius: 3px;
+    .loading-box_ {
+      position: absolute;
+      top: 0;
+      left: 0;
+      bottom: 0;
+      right: 0;
+      background-color: rgba(0, 0, 0, 0.8);
+      z-index: 20;
+      opacity: 1;
+      transition: opacity 200ms;
+      display: flex;
+      align-items: center;
+      justify-content: center;
+      &.hide_ {
+        opacity: 0;
+        pointer-events: none;
+        .loading-gif_ {
+          span {
+            animation-play-state: paused;
+          }
+        }
+      }
+      .loading-gif_ {
+        flex: none;
+        height: 5px;
+        line-height: 0;
+        @keyframes load {
+          0% {
+            opacity: 1;
+            transform: scale(1.3);
+          }
+          100% {
+            opacity: 0.2;
+            transform: scale(0.3);
+          }
+        }
+        span {
+          display: inline-block;
+          width: 5px;
+          height: 100%;
+          margin-left: 2px;
+          border-radius: 50%;
+          background-color: #888;
+          animation: load 1.04s ease infinite;
+          &:nth-child(1) {
+            margin-left: 0;
+          }
+          &:nth-child(2) {
+            animation-delay: 0.13s;
+          }
+          &:nth-child(3) {
+            animation-delay: 0.26s;
+          }
+          &:nth-child(4) {
+            animation-delay: 0.39s;
+          }
+          &:nth-child(5) {
+            animation-delay: 0.52s;
+          }
+        }
+      }
+    }
+    .info-box_ {
+      position: absolute;
+      bottom: 0;
+      left: 0;
+      width: 100%;
+      height: 24px;
+      line-height: 24px;
+      text-align: center;
+      overflow: hidden;
+      font-size: 13px;
+      background-color: #83ce3f;
+      opacity: 0;
+      transform: translateY(24px);
+      transition: all 200ms;
+      color: #fff;
+      z-index: 10;
+      &.show {
+        opacity: 0.95;
+        transform: translateY(0);
+      }
+      &.fail {
+        background-color: #ce594b;
+      }
+    }
+    .auth-canvas2_ {
+      position: absolute;
+      top: 0;
+      left: 0;
+      width: 60px;
+      height: 100%;
+      z-index: 4;
+    }
+    .auth-canvas3_ {
+      position: absolute;
+      top: 0;
+      left: 0;
+      opacity: 0;
+      transition: opacity 600ms;
+      z-index: 3;
+      &.show {
+        opacity: 1;
+      }
+    }
+    .flash_ {
+      position: absolute;
+      top: 0;
+      left: 0;
+      width: 30px;
+      height: 100%;
+      background-color: rgba(255, 255, 255, 0.1);
+      z-index: 3;
+      &.show {
+        transition: transform 600ms;
+      }
+    }
+    .reset_ {
+      position: absolute;
+      top: 2px;
+      right: 2px;
+      width: 35px;
+      height: auto;
+      z-index: 12;
+      cursor: pointer;
+      transition: transform 200ms;
+      transform: rotate(0deg);
+      &:hover {
+        transform: rotate(-90deg);
+      }
+    }
+  }
+  .auth-control_ {
+    .range-box {
+      position: relative;
+      width: 100%;
+      background-color: #eef1f8;
+      margin-top: 20px;
+      border-radius: 3px;
+      box-shadow: 0 0 8px rgba(240, 240, 240, 0.6) inset;
+      .range-text {
+        position: absolute;
+        top: 50%;
+        left: 50%;
+        transform: translate(-50%, -50%);
+        font-size: 14px;
+        color: #b7bcd1;
+        white-space: nowrap;
+        overflow: hidden;
+        text-overflow: ellipsis;
+        text-align: center;
+        width: 100%;
+      }
+      .range-slider {
+        position: absolute;
+        height: 100%;
+        width: 50px;
+        background-color: rgba(106, 160, 255, 0.8);
+        border-radius: 3px;
+        .range-btn {
+          position: absolute;
+          display: flex;
+          align-items: center;
+          justify-content: center;
+          right: 0;
+          width: 50px;
+          height: 100%;
+          background-color: #fff;
+          border-radius: 3px;
+          box-shadow: 0 0 4px #ccc;
+          cursor: pointer;
+          & > div {
+            width: 0;
+            height: 40%;
+
+            transition: all 200ms;
+            &:nth-child(2) {
+              margin: 0 4px;
+            }
+            border: solid 1px #6aa0ff;
+          }
+          &:hover,
+          &.isDown {
+            & > div:first-child {
+              border: solid 4px transparent;
+              height: 0;
+              border-right-color: #6aa0ff;
+            }
+            & > div:nth-child(2) {
+              border-width: 3px;
+              height: 0;
+              border-radius: 3px;
+              margin: 0 6px;
+              border-right-color: #6aa0ff;
+            }
+            & > div:nth-child(3) {
+              border: solid 4px transparent;
+              height: 0;
+              border-left-color: #6aa0ff;
+            }
+          }
+        }
+      }
+    }
+  }
+}
+.vue-puzzle-overflow {
+  overflow: hidden !important;
+}
+</style>

BIN
src/components/VSliderVerification/src/image/reset.png


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

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

+ 55 - 0
src/components/VTipTooltip/src/VTipTooltip.vue

@@ -0,0 +1,55 @@
+<script setup lang="ts">
+const props = withDefaults(
+  defineProps<{
+    label?: string
+    placement?: string
+    img?: string
+    width?: number
+  }>(),
+  {
+    placement: 'bottom',
+    width: 368
+  }
+)
+
+const visible = ref(false)
+</script>
+
+<template>
+  <div style="display: inline-block">
+    <el-tooltip
+      effect="dark"
+      popper-class="v-tip-tooltip"
+      v-model="visible"
+      :placement="props.placement"
+      trigger="click"
+    >
+      <span class="font_family icon-icon_info_b"></span>
+      <template #content>
+        <div class="label" :style="{ width: props.width + 'px' }">{{ props.label }}</div>
+        <div style="text-align: center">
+          <img :style="{ width: props.width + 'px' }" class="photo" :src="props.img" alt="" />
+        </div>
+      </template>
+    </el-tooltip>
+  </div>
+</template>
+
+<style lang="scss">
+div.v-tip-tooltip {
+  padding: 16px;
+  border-radius: 12px;
+  border: 0 !important;
+  box-shadow: 4px 4px 16px rgba(0, 0, 0, 0.1) !important;
+
+  .label {
+    color: #f0f1f3;
+    font-size: 14px;
+    white-space: wrap;
+  }
+  .photo {
+    margin-top: 8px;
+    border-radius: 6px;
+  }
+}
+</style>

+ 3 - 1
src/main.ts

@@ -14,6 +14,8 @@ import VXETablePluginExportXLSX from 'vxe-table-plugin-export-xlsx'
 import ExcelJS from 'exceljs'
 import { VLoading } from './directive/VLoading'
 
+import 'driver.js/dist/driver.css' // 导入样式
+
 import { createApp } from 'vue'
 import { createPinia } from 'pinia'
 
@@ -87,4 +89,4 @@ app.config.globalProperties.$toggleDarkMode = () => {
   }
 }
 
-app.mount('#app')
+app.mount('#app')

+ 226 - 0
src/stores/modules/guide.ts

@@ -0,0 +1,226 @@
+// store/guide.ts
+import { defineStore } from 'pinia'
+import { useDriver } from '@/utils/driverGuide'
+
+const oceanSteps: any = [
+  {
+    element: '#driver-step-tracking-detail-1',
+    popover: {
+      title: '',
+      description: 'Main operation button area',
+      side: 'bottom',
+      align: 'start'
+    }
+  },
+  {
+    element: '#driver-step-tracking-detail-2',
+    popover: {
+      description: 'Key shipment status',
+      side: 'bottom',
+      align: 'start'
+    }
+  },
+  {
+    element: '#driver-step-tracking-detail-3',
+    popover: {
+      description: 'Detail container status of each container',
+      side: 'bottom',
+      align: 'start'
+    }
+  },
+  {
+    element: '#tracking-map',
+    popover: {
+      description: `
+        <ul>
+          <li>Actual Line(Actual Trajectory)</li>
+          <li>Virtual Line(Planned Trajectory)</li>
+          <li>Arrow (Current Real-time Position)</li>
+        </ol>
+      `,
+      side: 'bottom',
+      align: 'start'
+    }
+  },
+  {
+    element: '#driver-step-tracking-detail-5',
+    popover: {
+      description: `
+      <ul>
+        <li>Upload Files</li>
+        <li>Clickable download icons to download files</li>
+      </ol>
+      `,
+      side: 'bottom',
+      align: 'start'
+    }
+  },
+  {
+    element: '#driver-step-tracking-detail-6',
+    popover: {
+      description: `
+        <p>Send email to site staff</p>
+        <p>Enter contents you want to communicate with, click “Send 
+         Email” button to send out.
+        </p>
+      `,
+      side: 'bottom',
+      align: 'start'
+    }
+  },
+  {
+    element: '#page-guide-btn-guide',
+    popover: {
+      title: '',
+      description:
+        'After closing, you can still click the "Page Guide" button to view the page guide of the current page.',
+      side: 'bottom'
+    }
+  }
+]
+const airSteps: any = [
+  {
+    element: '#driver-step-tracking-detail-1',
+    popover: {
+      title: '',
+      description: 'Main operation button area',
+      side: 'bottom',
+      align: 'start'
+    }
+  },
+  {
+    element: '#driver-step-tracking-detail-2',
+    popover: {
+      description: 'Key shipment status',
+      side: 'bottom',
+      align: 'start'
+    }
+  },
+  {
+    element: '#tracking-map',
+    popover: {
+      description: `
+        <ul>
+          <li>Actual Line(Actual Trajectory)</li>
+          <li>Virtual Line(Planned Trajectory)</li>
+          <li>Arrow (Current Real-time Position)</li>
+        </ol>
+      `,
+      side: 'bottom',
+      align: 'start'
+    }
+  },
+  {
+    element: '#driver-step-tracking-detail-5',
+    popover: {
+      description: `
+      <ul>
+        <li>Upload Files</li>
+        <li>Clickable download icons to download files</li>
+      </ol>
+      `,
+      side: 'bottom',
+      align: 'start'
+    }
+  },
+  {
+    element: '#driver-step-tracking-detail-6',
+    popover: {
+      description: `
+        <p>Send email to site staff</p>
+        <p>Enter contents you want to communicate with, click “Send 
+         Email” button to send out.
+        </p>
+      `,
+      side: 'bottom',
+      align: 'start'
+    }
+  },
+  {
+    element: '#page-guide-btn-guide',
+    popover: {
+      title: '',
+      description:
+        'After closing, you can still click the "Page Guide" button to view the page guide of the current page.',
+      side: 'bottom'
+    }
+  }
+]
+
+const guideTimer = ref<ReturnType<typeof setTimeout> | null>(null)
+export const useGuideStore = defineStore('guide', {
+  state: () => ({
+    booking: {
+      isShowMoreFiltersGuidePhoto: false,
+      isShowDownloadFileGuidePhoto: false,
+      isShowFilterGuidePhoto: false,
+      isShowCustomizeColumnsGuidePhoto: false
+    },
+    tracking: {
+      isShowFilterGuidePhoto: false,
+      isShowMoreFiltersGuidePhoto: false,
+      isShowDownloadFileGuidePhoto: false,
+      isShowCustomizeColumnsGuidePhoto: false
+    },
+    dashboard: {
+      isShowViewManagementGuidePhoto: false,
+      isShowTransportModeGuidePhoto: false,
+      isShowSaveConfigGuidePhoto: false,
+      isShowKpiChartGuidePhoto: false
+    },
+    trackingDetail: {
+      mode: 'Ocean Freight'
+    }
+  }),
+  actions: {
+    resetGuide(key: string) {
+      if (this.booking[key] !== undefined) {
+        this.booking[key] = false
+      } else if (this.tracking[key] !== undefined) {
+        this.tracking[key] = false
+      } else if (this.dashboard[key] !== undefined) {
+        this.dashboard[key] = false
+      }
+    },
+    handleTrackingDetailGuide() {
+      let steps = []
+      if (this.trackingDetail.mode === 'Ocean Freight') {
+        steps = oceanSteps
+      } else if (this.trackingDetail.mode === 'Air Freight') {
+        steps = airSteps
+      } else {
+        return
+      }
+      const { start, movePrevious, hasNextStep, moveTo, destroy } = useDriver(steps, {
+        onPrevClick: () => {
+          if (guideTimer.value) {
+            clearTimeout(guideTimer.value)
+            guideTimer.value = null
+          }
+          movePrevious()
+        },
+        onHighlightStarted: () => {
+          if (!hasNextStep()) {
+            guideTimer.value = setTimeout(() => {
+              destroy()
+            }, 3000)
+          }
+        },
+        onDestroyStarted: (element, step, options) => {
+          if (hasNextStep()) {
+            moveTo(options.config.steps.length - 1)
+            return
+          }
+          destroy()
+        },
+        onDestroyed: () => {
+          if (guideTimer.value) {
+            clearTimeout(guideTimer.value)
+            guideTimer.value = null
+          }
+        }
+      })
+      start() // 开始引导
+    }
+  }
+})

+ 84 - 0
src/styles/driver.scss

@@ -0,0 +1,84 @@
+div.driver-popover {
+  width: 400px;
+  max-width: 400px;
+  padding: 15px;
+  padding-top: 27px;
+  padding-bottom: 8px;
+  background-image: linear-gradient(96deg, #b58eff 2.25%, #fdbc94 97.98%);
+  box-shadow: 4px 4px 16px rgba(0, 0, 0, 0.1);
+  border-radius: 12px;
+}
+// 标题
+header.driver-popover-title {
+  color: var(--color-neutral-1);
+}
+
+// 角标
+div.driver-popover-arrow-side-left {
+  border-left-color: #b78ffc;
+}
+div.driver-popover-arrow-side-right {
+  border-right-color: #b78ffc;
+}
+div.driver-popover-arrow-side-top {
+  border-top-color: #b78ffc;
+}
+div.driver-popover-arrow-side-bottom {
+  border-bottom-color: #b78ffc;
+}
+
+// 遮罩
+.driver-overlay.driver-overlay-animated {
+  path {
+    fill: var(--color-tour-mask-bg) !important;
+  }
+}
+// 关闭图标
+button.driver-popover-close-btn {
+  top: 3px;
+  right: 4px;
+  color: #2b2f36;
+  &:hover,
+  &:focus {
+    color: #2b2f36;
+  }
+}
+// 内容
+div.driver-popover-description {
+  color: #2b2f36;
+  text-align: left;
+  ul {
+    margin-left: 16px;
+    list-style: disc;
+    li {
+      color: #2b2f36;
+    }
+  }
+}
+
+// 页数
+span.driver-popover-progress-text {
+  color: rgba($color: #fff, $alpha: 0.7);
+  font-size: 12px;
+}
+footer.driver-popover-footer {
+  margin-top: 28px;
+  height: 36px;
+  .driver-popover-navigation-btns {
+    // 上一步
+    button.driver-popover-prev-btn {
+      width: 96px;
+      height: 36px;
+      padding: 0;
+      background-color: transparent;
+      border: none;
+    }
+    button.driver-popover-next-btn {
+      width: 96px;
+      height: 36px;
+      padding: 0;
+      background-color: transparent;
+      border: none;
+    }
+  }
+}

+ 2 - 2
src/styles/elementui.scss

@@ -815,7 +815,7 @@ div .avatar_bg {
   background-color: transparent !important;
 }
 div .carousel .el-carousel__indicator--horizontal {
-  padding: 4px 8px;
+  padding: 4px;
 }
 div .carousel .el-carousel__button {
   width: 6px;
@@ -831,7 +831,7 @@ div .carousel .el-carousel__item--card, .el-carousel__item.is-animating {
   display: flex;
   align-items: center;
   justify-content: center;
-  padding-bottom: 15px;
+  padding-bottom: 9px;
 }
 div .carousel .el-carousel__arrow {
   opacity: 1;

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

@@ -1,9 +1,9 @@
 @font-face {
   font-family: "font_family"; /* Project id 4672385 */
-  src: url('iconfont.woff2?t=1745286986564') format('woff2'),
-       url('iconfont.woff?t=1745286986564') format('woff'),
-       url('iconfont.ttf?t=1745286986564') format('truetype'),
-       url('iconfont.svg?t=1745286986564#font_family') format('svg');
+  src: url('iconfont.woff2?t=1750149505564') format('woff2'),
+       url('iconfont.woff?t=1750149505564') format('woff'),
+       url('iconfont.ttf?t=1750149505564') format('truetype'),
+       url('iconfont.svg?t=1750149505564#font_family') format('svg');
 }
 
 .font_family {
@@ -14,6 +14,22 @@
   -moz-osx-font-smoothing: grayscale;
 }
 
+.icon-icon_guidelines_b:before {
+  content: "\e719";
+}
+
+.icon-icon_cancelled_b:before {
+  content: "\e71a";
+}
+
+.icon-icon_configurations_b:before {
+  content: "\e718";
+}
+
+.icon-a-1:before {
+  content: "\e717";
+}
+
 .icon-icon_good_b:before {
   content: "\e713";
 }

Diff do ficheiro suprimidas por serem muito extensas
+ 0 - 0
src/styles/icons/iconfont.js


Diff do ficheiro suprimidas por serem muito extensas
+ 6 - 0
src/styles/icons/iconfont.svg


BIN
src/styles/icons/iconfont.ttf


BIN
src/styles/icons/iconfont.woff


BIN
src/styles/icons/iconfont.woff2


+ 6 - 8
src/styles/index.scss

@@ -4,6 +4,7 @@
 @import './theme.scss';
 @import './Antdui.scss';
 @import './icons/iconfont.css';
+@import './driver.scss';
 
 #app {
   font-size: var(--font-size-3);
@@ -56,12 +57,9 @@
 .icon_dark {
   fill: var(--color-neutral-1);
 }
-div.markdown-body {
-  background: transparent;
-  table {
-    th,
-    td {
-      white-space: nowrap;
-    }
-  }
+
+.position-absolute-guide {
+  position: absolute;
+  z-index: 1200;
+  // box-shadow: 3px 3px 16px 0px rgba(0, 0, 0, 0.2);
 }

+ 64 - 12
src/styles/reset.scss

@@ -134,24 +134,44 @@ 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%;
-  line-height: 1.6;
-  font-size: 16px;
   color: #333;
 }
 
-.markdown-body h1,
-.markdown-body h2,
-.markdown-body h3,
-.markdown-body h4,
-.markdown-body h5,
-.markdown-body h6 {
+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;
 }
@@ -162,16 +182,36 @@ div {
   padding-left: 1em;
 }
 
-.markdown-body ul {
+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 {
@@ -181,9 +221,21 @@ div {
   margin: 1em 0;
 }
 
-.markdown-body a {
-  color: #0366d6;
-  text-decoration: underline;
+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 {

+ 2 - 1
src/styles/theme-g.scss

@@ -58,6 +58,7 @@
   // 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);
 
@@ -90,7 +91,7 @@
   --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: #FCEEE3;
+  --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);

+ 23 - 0
src/styles/theme.scss

@@ -169,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;
 
@@ -316,6 +317,16 @@
   --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;
+  --color-tour-next-btn-hover-bg: #2b2f36;
+  --color-tour-next-btn-color: #fff;
+  --color-tour-step-color: #b5b9bf;
+  --color-tour-mask-bg: rgba(43, 47, 54, 0.7);
+
+  --color-guide-icon-bg: rgba(237, 237, 237, 0.6);
+  --color-guide-icon-hover-bg: rgba(237, 237, 237, 0.45);
 }
 
 :root.dark {
@@ -325,6 +336,7 @@
   --color-neutral-1: #f0f1f3;
   --color-neutral-2: rgba(240, 241, 243, 0.7);
   --color-neutral-3: rgba(240, 241, 243, 0.3);
+
   --color-border: #3f434a;
   --color-header-bg: #30353c;
 
@@ -509,4 +521,15 @@
       border-color: #3f434a;
     }
   }
+
+  --color-tour-popover-bg: #cf5f00;
+  --color-tour-prev-btn-border: #e6c5aa;
+  --color-tour-next-btn-bg: #f0f1f3;
+  --color-tour-next-btn-hover-bg: #e3e5e8;
+  --color-tour-next-btn-color: #ed6d00;
+  --color-tour-step-color: rgba(240, 241, 243, 0.7);
+  --color-tour-mask-bg: rgba(0, 0, 0, 0.7);
+
+  --color-guide-icon-bg: rgba(237, 237, 237, 0.1);
+  --color-guide-icon-hover-bg: rgba(237, 237, 237, 0.15);
 }

+ 2 - 0
src/utils/axios.ts

@@ -2,6 +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 emitter from '@/utils/bus'
 
 interface codeMessage {
   [key: number]: string
@@ -61,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',

+ 62 - 0
src/utils/driverGuide.ts

@@ -0,0 +1,62 @@
+import { driver, type DriveStep, type Config } from 'driver.js'
+import 'driver.js/dist/driver.css'
+import nextBtnImg from '@/assets/image/next-btn.png'
+import previousBtnImg from '@/assets/image/pervious-btn.png'
+import doneBtnImg from '@/assets/image/done-btn.png'
+
+/**
+ * useDriver composable
+ * @param steps 引导步骤数组
+ * @param globalConfig 可选的 driver 配置
+ */
+export function useDriver(steps: DriveStep[], globalConfig?: Config) {
+  const driverInstance = driver({
+    ...globalConfig,
+    animate: false,
+    showProgress: true,
+    overlayOpacity: 0.7,
+    overlayColor: '#2b2f36',
+    showButtons: ['previous', 'next', 'close'],
+    onPopoverRender: (popoverDOM, { config, state, driver }) => {
+      globalConfig?.onPopoverRender?.(popoverDOM, { config, state, driver })
+
+      const stepsLength = steps.length
+      const nextBtn = popoverDOM.nextButton
+      if (nextBtn) {
+        nextBtn.innerHTML = '' // 清空原内容
+        const img = document.createElement('img')
+        img.src = stepsLength - 1 === state.activeIndex ? doneBtnImg : nextBtnImg // 替换成你的图片路径
+        img.alt = 'next'
+        img.style.width = '96px'
+        img.style.height = '36px'
+        nextBtn.appendChild(img)
+      }
+
+      const previousBtn = popoverDOM.previousButton
+      if (previousBtn) {
+        previousBtn.innerHTML = '' // 清空原内容
+        const img = document.createElement('img')
+        img.src = previousBtnImg // 替换成你的图片路径
+        img.alt = 'previous'
+        img.style.width = '96px'
+        img.style.height = '36px'
+        previousBtn.appendChild(img)
+        img.style.display = state.activeIndex > 0 ? 'block' : 'none' // 如果是第一个步骤,则隐藏上一页按钮
+      }
+    }
+  })
+
+  driverInstance.setSteps(steps)
+
+  return {
+    start: (stepIndex = 0) => driverInstance.drive(stepIndex),
+    moveNext: () => driverInstance.moveNext(),
+    movePrevious: () => driverInstance.movePrevious(),
+    highlight: (step: DriveStep) => driverInstance.highlight(step),
+    destroy: () => driverInstance.destroy(),
+    isActive: () => driverInstance.isActive(),
+    hasNextStep: () => driverInstance.hasNextStep(),
+    moveTo: (stepIndex: number) => driverInstance.moveTo(stepIndex),
+    driverInstance
+  }
+}

+ 29 - 23
src/views/AIApiLog/src/AIApiLog.vue

@@ -1,31 +1,27 @@
 <script lang="ts" setup>
 import { useCalculatingHeight } from '@/hooks/calculatingHeight'
 import TableView from './components/TableView'
+import dayjs from 'dayjs'
 
-const OperationSearch = ref()
 const filterRef: Ref<HTMLElement | null> = ref(null)
 const containerHeight = useCalculatingHeight(document.documentElement, 290, [filterRef])
 const searchData = ref({
-  inputModel: '',
-  startDate: '',
-  endDate: '',
-  aiModel: '',
-  comparator: 'thanOrEqual',
-  responseDuration: 0
+  text_search: '',
+  request_date_start: '',
+  request_date_end: '',
+  ai_model: '',
+  response_duration_type: '',
+  response_duration_num: null
 })
 
 const aiModelList = [
   {
-    label: 'Deepseek-chat',
-    value: 'deepseekChat'
+    label: 'Deepseek',
+    value: 'Deepseek'
   },
   {
-    label: 'Deepseek-search',
-    value: 'deepseekSearch'
-  },
-  {
-    label: 'Claude 3.7 Sonnet',
-    value: 'claude'
+    label: 'Claude',
+    value: 'Claude'
   }
 ]
 
@@ -50,8 +46,13 @@ const Search = () => {
   tableRef.value.SearchOperationLog(searchData.value)
 }
 const DateChange = (date: any) => {
-  searchData.value.startDate = date[0]
-  searchData.value.endDate = date[1]
+  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>
@@ -63,7 +64,7 @@ const DateChange = (date: any) => {
         <div class="input-tips_filter">
           <el-input
             placeholder="Search Request ID、Question ID"
-            v-model="OperationSearch"
+            v-model="searchData.text_search"
             class="log_input"
           >
             <template #prefix>
@@ -80,7 +81,7 @@ const DateChange = (date: any) => {
           <CalendarDate @DateChange="DateChange"></CalendarDate>
         </div>
         <div class="tips_filter">
-          <el-select v-model="searchData.aiModel" placeholder="AI Model">
+          <el-select v-model="searchData.ai_model" clearable placeholder="AI Model">
             <el-option
               v-for="item in aiModelList"
               :key="item.value"
@@ -91,7 +92,12 @@ const DateChange = (date: any) => {
         </div>
         <div class="comparator-tips_filter">
           <span>Response Duration</span>
-          <el-select v-model="searchData.comparator" style="width: 70px; margin: 0 6px">
+          <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"
@@ -101,11 +107,11 @@ const DateChange = (date: any) => {
             </el-option>
           </el-select>
           <el-input-number
-            v-model="searchData.responseDuration"
-            placeholder="s"
+            v-model="searchData.response_duration_num"
+            placeholder=""
             :controls="false"
             :min="0"
-            style="width: 60px"
+            style="width: 60px; height: 34px"
           ></el-input-number>
         </div>
 

+ 39 - 29
src/views/AIApiLog/src/components/LogDialog.vue

@@ -1,8 +1,29 @@
 <script setup lang="ts">
+import VueJsonPretty from 'vue-json-pretty'
+import 'vue-json-pretty/lib/styles.css'
+
 const dialogVisible = ref(false)
 
-const openDialog = () => {
+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
@@ -10,38 +31,26 @@ defineExpose({
 </script>
 
 <template>
-  <el-dialog v-model="dialogVisible" class="log-dialog" title="AI API Log" width="1000" top="10vh">
+  <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">
-        Hello, I would like to check the status of my package. The tracking number is ABC123456789.
+      <div class="content" ref="requestContentRef">
+        {{ requestContent }}
       </div>
     </div>
     <el-divider style="margin: 16px 0" />
     <div class="response-section">
-      <div class="title">Request Content</div>
-      <p class="content">
-        Hey there! For the package with tracking number ABC123456789, the latest update is: [insert
-        latest tracking status here, e.g., shipped, in transit, arrived at the delivery hub, out for
-        delivery, etc.]. You can see more details by clicking this link: [insert detailed tracking
-        link here].Hey there! For the package with tracking number ABC123456789, the latest update
-        is: [insert latest tracking status here, e.g., shipped, in transit, arrived at the delivery
-        hub, out for delivery, etc.]. You can see more details by clicking this link: [insert
-        detailed tracking link here].Hey there! For the package with tracking number ABC123456789,
-        the latest update is: [insert latest tracking status here, e.g., shipped, in transit,
-        arrived at the delivery hub, out for delivery, etc.]. You can see more details by clicking
-        this link: [insert detailed tracking link here].Hey there! For the package with tracking
-        number ABC123456789, the latest update is: [insert latest tracking status here, e.g.,
-        shipped, in transit, arrived at the delivery hub, out for delivery, etc.]. You can see more
-        details by clicking this link: [insert detailed tracking link here].Hey there! For the
-        package with tracking number ABC123456789, the latest update is: [insert latest tracking
-        status here, e.g., shipped, in transit, arrived at the delivery hub, out for delivery,
-        etc.]. You can see more details by clicking this link: [insert detailed tracking link
-        here].Hey there! For the package with tracking number ABC123456789, the latest update is:
-        [insert latest tracking status here, e.g., shipped, in transit, arrived at the delivery hub,
-        out for delivery, etc.]. You can see more details by clicking this link: [insert detailed
-        tracking link here].
-      </p>
+      <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>
@@ -67,12 +76,13 @@ defineExpose({
   .content {
     padding: 8px 16px 20px 16px;
     border-radius: 6px;
+    overflow: auto;
     background-color: var(--color-share-link-bg);
   }
 }
 </style>
-<style>
-.log-dialog {
+<style lang="scss">
+.ai-api-log-dialog {
   height: 80%;
   .el-dialog__body {
     padding: 0;

+ 43 - 29
src/views/AIApiLog/src/components/TableView/src/TableView.vue

@@ -2,7 +2,7 @@
 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 { autoWidth } from '@/utils/table'
 import { useRowClickStyle } from '@/hooks/rowClickStyle'
 import dayjs from 'dayjs'
 import { formatTimezone, formatNumber } from '@/utils/tools'
@@ -60,18 +60,17 @@ const handleColumns = (columns: any, status?: string) => {
 // 获取表格列
 const getTableColumns = async () => {
   tableLoadingColumn.value = true
-  await $api.getOperationTableColumns().then((res: any) => {
+  await $api.getAIApiLogTableColumn().then((res: any) => {
     if (res.code === 200) {
       tableData.value.columns = [
         { type: 'checkbox', width: 50, fixed: 'left' },
-        ...handleColumns(res.data.OperationTableColumns),
-        { title: 'Action', width: 106, fixed: 'right', slots: { default: 'action' } }
+        ...handleColumns(res.data.OperationTableColumns)
       ]
       tableOriginColumnsField.value = res.data.OperationTableColumns
     }
   })
   nextTick(() => {
-    // tableRef.value && autoWidth(tableData.value, tableRef.value)
+    tableRef.value && autoWidth(tableData.value, tableRef.value)
     tableLoadingColumn.value = false
     selectedNumber.value = 0
     selectedTableData.value = []
@@ -91,7 +90,7 @@ const assignTableData = (data: any) => {
     allTable.value.data = data.searchData || []
     // 为了让导出的表格列宽度自适应
     nextTick(() => {
-      // allTableRef.value && autoWidth(allTable.value, allTableRef.value)
+      allTableRef.value && autoWidth(allTable.value, allTableRef.value)
     })
   }, 1000)
 }
@@ -102,7 +101,7 @@ const getTableData = async (isPageChange?: boolean) => {
   const rc = isPageChange ? pageInfo.value.total : -1
   tableLoadingTableData.value = true
   await $api
-    .SearchOperationLog({
+    .getAIApiLogTableData({
       cp: pageInfo.value.pageNo,
       ps: pageInfo.value.pageSize,
       rc,
@@ -117,7 +116,7 @@ const getTableData = async (isPageChange?: boolean) => {
       selectedNumber.value = 0
       selectedTableData.value = []
       nextTick(() => {
-        // tableRef.value && autoWidth(tableData.value, tableRef.value)
+        tableRef.value && autoWidth(tableData.value, tableRef.value)
         tableLoadingTableData.value = false
       })
     })
@@ -126,7 +125,7 @@ const SearchOperationLog = (val: any) => {
   searchdata = val
   tableLoadingTableData.value = true
   $api
-    .SearchOperationLog({
+    .getAIApiLogTableData({
       cp: pageInfo.value.pageNo,
       ps: pageInfo.value.pageSize,
       rc: -1,
@@ -141,7 +140,7 @@ const SearchOperationLog = (val: any) => {
       selectedNumber.value = 0
       selectedTableData.value = []
       nextTick(() => {
-        // tableRef.value && autoWidth(tableData.value, tableRef.value)
+        tableRef.value && autoWidth(tableData.value, tableRef.value)
         tableLoadingTableData.value = false
       })
     })
@@ -149,7 +148,7 @@ const SearchOperationLog = (val: any) => {
 onMounted(() => {
   Promise.all([getTableColumns(), getTableData(false)]).finally(() => {
     nextTick(() => {
-      // tableRef.value && autoWidth(tableData.value, tableRef.value)
+      tableRef.value && autoWidth(tableData.value, tableRef.value)
     })
   })
 })
@@ -276,7 +275,7 @@ const getExportTableData = (status: number) => {
     column = buildColumnString(allTable.value.columns)
   }
   $api
-    .OperationLogDownload({
+    .getAIApiLogAllTableData({
       selected_fields: column,
       tmp_search: tempSearch.value
     })
@@ -299,7 +298,7 @@ const exportTable = (status: number) => {
   const exportConfig: any = {
     type: 'xlsx',
     message: false,
-    filename: `Chat Log_${dayjs().format('YYYYMMDDHH[h]mm[m]ss[s]')}`
+    filename: `AI API Log_${dayjs().format('YYYYMMDDHH[h]mm[m]ss[s]')}`
   }
   if (status === 1) {
     exportConfig.columnFilterMethod = ({ column }: any) => {
@@ -336,8 +335,21 @@ const handleCheckAllChange = ({ records }: any) => {
 }
 
 const logDialogRef = ref()
-const handleLogDetail = (row) => {
-  logDialogRef.value.openDialog(row)
+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({
@@ -354,7 +366,13 @@ defineExpose({
     element-loading-custom-class="element-loading"
     element-loading-background="rgb(43, 47, 54, 0.7)"
   >
-    <div class="table-tools">
+    <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">
@@ -376,18 +394,10 @@ defineExpose({
       <template #empty v-if="!tableLoadingTableData && tableData.data.length === 0">
         <VEmpty></VEmpty>
       </template>
-      <!-- action操作栏的插槽 -->
-      <template #action="{ row }">
-        <el-button
-          style="height: 24px; padding: 8px 4px; padding-left: 5px; font-size: 12px"
-          @click="handleLogDetail(row)"
-        >
-          <span
-            style="margin-right: 2px; font-size: 15px"
-            class="font_family icon-icon_ai_api_log_b"
-          ></span
-          >AI API Log
-        </el-button>
+      <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>
@@ -407,7 +417,11 @@ defineExpose({
         />
       </div>
     </div>
-    <DownloadDialog @export="getExportTableData" ref="downloadDialogRef" />
+    <DownloadDialog
+      @export="getExportTableData"
+      :isHideSelectColumn="true"
+      ref="downloadDialogRef"
+    />
     <LogDialog ref="logDialogRef" />
   </div>
 </template>

+ 7 - 1
src/views/AIApiLog/src/components/TableView/src/components/DownloadDialog.vue

@@ -1,4 +1,10 @@
 <script setup lang="ts">
+const props = withDefaults(
+  defineProps<{
+    isHideSelectColumn: boolean
+  }>(),
+  { isHideSelectColumn: false }
+)
 const dialogVisible = ref(false)
 
 const openDialog = (selectedColumns: string[], slectedDataNumber: number) => {
@@ -41,7 +47,7 @@ defineExpose({
             }}</span>
           </div>
         </div>
-        <div class="download-filter">
+        <div class="download-filter" v-if="!props.isHideSelectColumn">
           <el-radio-group v-model="downloadFilter">
             <el-radio :value="1"
               >Download with selected columns

+ 195 - 59
src/views/AIRobotChat/src/AIRobotChat.vue

@@ -12,6 +12,7 @@ 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,
@@ -20,7 +21,6 @@ const md = new MarkdownIt({
 })
 
 const renderedMessage = (content) => {
-  // console.log('content', content)
   if (!content) {
     return ''
   }
@@ -49,7 +49,7 @@ const isShowFooterShadow = ref(false)
 // 是否显示header的底部阴影
 const isShowHeaderShadow = ref(false)
 
-const loadingAnswer = ref(false) // 是否正在加载答案
+const loadingAnswer = ref(false) // 是否正在请求答案
 interface MessageItem {
   id?: string // 唯一标识
   type: 'robot' | 'user'
@@ -97,9 +97,14 @@ const progressStatus = {
 
 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
@@ -111,6 +116,7 @@ const parseHtmlString = (data) => {
         index++
         scrollToBottom() // 滚动到底部
       } else {
+        isShowLoadingDots.value = false // 停止显示加载点
         clearInterval(timer)
       }
     }, 150)
@@ -123,24 +129,34 @@ 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 ? 'Predefined Question' : 'Free Question',
-      // question_type: 'Free Question',
+      question_type:
+        isPresetQuestion || !is_FixedAnswer.value ? 'Predefined Question' : 'Free Question',
       question_content: question,
-      fixed_faq: !is_FixedAnswer ? messages.value[messages.value.length - 2].content : ''
+      fixed_faq: fixed_faq
     })
     .then((res) => {
-      if (isPause.value) {
+      if (isPause.value || queryTime.value === -1) {
         return
       }
       if (res.code === 200) {
         clearInterval(progressInterval.value)
 
-        is_FixedAnswer.value = res.data.is_fixedAnswer_end || false
+        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,
@@ -158,6 +174,9 @@ const aiChat = (question, isPresetQuestion) => {
       } 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
       }
     })
 }
@@ -179,15 +198,16 @@ const handleSend = (question, isPresetQuestion = true, isExternal = false) => {
   }
   isPause.value = false
   loadingAnswer.value = true
-
-  aiChat(question, isPresetQuestion)
   // 将用户内容添加到消息列表
   messages.value.push({
     type: 'user',
-    content: question
+    content: question,
+    isAnswer: true
   })
   !isPresetQuestion ? (userQuestion.value = '') : ''
   queryTime.value = 0
+
+  aiChat(question, isPresetQuestion)
   messages.value.push({
     type: 'robot',
     content: progressStatus[0]
@@ -218,6 +238,7 @@ const handleSend = (question, isPresetQuestion = true, isExternal = false) => {
     }
   }, 1000)
 }
+
 const showScrollButton = ref(false) // 控制按钮显示
 const messagesRef = ref()
 const autoScroll = ref(true)
@@ -236,7 +257,8 @@ function handleScroll() {
 
   isShowHeaderShadow.value = !!messagesRef.value?.scrollTop
 }
-function scrollToBottom() {
+const scrollToBottom = (isScroll = false) => {
+  if (!isScroll && (!autoScroll.value || !messagesRef.value)) return
   nextTick(() => {
     if (messagesRef.value) {
       messagesRef.value.scrollTop = messagesRef.value.scrollHeight
@@ -280,6 +302,13 @@ const handleFeedback = (index, feedback) => {
       }
     })
 }
+// 判断是否展示评价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'])
 // 关闭聊天窗口
@@ -288,10 +317,34 @@ const handleClose = () => {
   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
+  handleOpen,
+  clearData,
+  handelclckaiinit
 })
+
 </script>
 
 <template>
@@ -300,20 +353,38 @@ defineExpose({
       <div class="header">
         <span class="welcome">Hi! I'm your Freight Assistant</span>
         <div class="option-icon">
-          <span
-            v-if="modalSize === 'large'"
-            class="font_family icon-icon_sidebar__window_b"
-            @click="modalSize = 'small'"
-          ></span>
-          <span
-            v-else-if="modalSize !== 'large'"
-            class="font_family icon-icon_maximized__window_b"
-            @click="modalSize = 'large'"
-          ></span>
-          <span @click="handleClose" class="font_family icon-icon_collapsed__to_widget_b"></span>
+          <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 :modalSize="modalSize" @question="handleSend"></AIQuestions>
+      <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>
@@ -353,10 +424,15 @@ defineExpose({
           <div v-html="msg.html || renderedMessage(msg.content)" class="markdown-body"></div>
         </div>
         <LoadingDots
-          v-if="index === messages.length - 1 && msg.isAnswer && loadingAnswer"
+          v-if="
+            index === messages.length - 1 &&
+            msg.isAnswer &&
+            isShowLoadingDots &&
+            msg.type === 'robot'
+          "
         ></LoadingDots>
         <!-- 评价  -->
-        <div class="review" v-if="msg.isShowFeedback && msg.isAnswer">
+        <div class="review" v-if="shouldShowFeedback(msg, index)">
           <el-button
             v-if="msg.feedback !== 'Cood'"
             class="el-button--text"
@@ -366,7 +442,7 @@ defineExpose({
           </el-button>
           <div v-if="msg.feedback === 'Cood'" style="width: 16px; text-align: center">
             <span
-              style="color: var(--color-theme); font-size: 14px"
+              style="color: var(--color-theme)"
               class="font_family icon-icon_good__filled_b"
             ></span>
           </div>
@@ -379,7 +455,7 @@ defineExpose({
           </el-button>
           <div v-if="msg.feedback === 'Not Good'" style="width: 16px; text-align: center">
             <span
-              style="color: var(--color-theme); font-size: 14px"
+              style="color: var(--color-theme)"
               class="font_family icon-icon_notgood__filled_b"
             ></span>
           </div>
@@ -388,16 +464,18 @@ defineExpose({
         <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 -->
-        <div
-          class="pause-btn"
+        <el-tooltip
           v-if="index === messages.length - 1 && queryTime > 29 && queryTime < 120"
-          @click="handlePause"
-        >
-          <div class="dot"></div>
-        </div>
+          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">
+      <div v-if="showScrollButton" class="scroll-to-bottom-btn" @click="scrollToBottom(true)">
         <span class="font_family icon-icon_movedown_b"></span>
       </div>
     </div>
@@ -422,17 +500,61 @@ defineExpose({
           <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: 74px;
+  top: 24px;
   right: 24px;
-  height: calc(100% - 98px);
-  z-index: 4000;
+  height: calc(100% - 48px);
+  z-index: 2000;
   display: flex;
   flex-direction: column;
   border-radius: 12px;
@@ -466,7 +588,6 @@ defineExpose({
       .option-icon {
         display: flex;
         align-items: center;
-        gap: 6px;
         .font_family {
           font-size: 16px;
           cursor: pointer;
@@ -519,22 +640,15 @@ defineExpose({
     .message-item {
       position: relative;
       display: inline-block;
-      padding: 11px 8px;
+      padding: 12px 16px;
       margin-bottom: 7px;
       border-radius: 12px;
       background-color: var(--scoring-bg-color);
       .review {
-        position: absolute;
-        bottom: -24px;
-        left: 0;
         display: flex;
         align-items: center;
-        gap: 13px;
-        width: 100%;
-        height: 30px;
-        margin-top: 10px;
-        padding-left: 30px;
-        padding-top: 5px;
+        gap: 16px;
+        margin-top: 12px;
 
         button.el-button + .el-button {
           margin-left: 0px;
@@ -563,7 +677,6 @@ defineExpose({
         width: 16px;
         span {
           color: var(--color-neutral-2);
-          font-size: 14px;
         }
         &:hover {
           span {
@@ -591,7 +704,7 @@ defineExpose({
         height: 16px;
         width: 16px;
         border-radius: 50%;
-        background-color: var(--color-customize-column-right-section-bg);
+        background-color: var(--color-pause-btn-bg);
         .dot {
           height: 5px;
           width: 5px;
@@ -600,11 +713,7 @@ defineExpose({
         }
       }
     }
-    .query-style {
-      span {
-        color: #b5b9bf;
-      }
-    }
+
     .robot-bubble {
       background: var(--scoring-bg-color);
       align-self: flex-start;
@@ -631,7 +740,7 @@ defineExpose({
       position: absolute;
       right: 50%;
       transform: translateX(50%);
-      bottom: 58px;
+      bottom: 76px;
       display: flex;
       justify-content: center;
       align-items: center;
@@ -641,7 +750,7 @@ defineExpose({
       // 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(1px); /* 应用10px的模糊效果 */
+      backdrop-filter: blur(1.7px); /* 应用10px的模糊效果 */
       span {
         color: #2b2f36;
       }
@@ -698,6 +807,20 @@ defineExpose({
       }
     }
   }
+  .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% {
@@ -709,4 +832,17 @@ defineExpose({
     }
   }
 }
+: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>

+ 10 - 2
src/views/AIRobotChat/src/components/AIQuestions.vue

@@ -98,6 +98,10 @@ onMounted(() => {
   AIRobotInit()
 })
 
+defineExpose({
+  AIRobotInit
+})
+
 const emit = defineEmits<{ question: [string] }>()
 const clickQuestion = (question) => {
   emit('question', question)
@@ -120,7 +124,7 @@ const clickQuestion = (question) => {
         v-if="props.modalSize === 'large'"
         class="carousel large_carousel"
         :autoplay="false"
-        height="115px"
+        height="105px"
       >
         <el-carousel-item v-for="(page, index) in pages" :key="index">
           <div class="dialogue_container dialogue_container_large">
@@ -190,7 +194,7 @@ const clickQuestion = (question) => {
   color: transparent;
   font-size: 14px;
   font-weight: 700;
-  margin: 5px 0 0 55px;
+  margin: 1px 0 0 55px;
 }
 .small_carousel {
   width: 452px;
@@ -239,4 +243,8 @@ const clickQuestion = (question) => {
 :deep(.el-carousel__item--card, .el-carousel__item.is-animating) {
   padding-bottom: 0 !important;
 }
+:deep(.el-carousel__indicators--horizontal) {
+  bottom: -2px;
+  height: 16px;
+}
 </style>

+ 16 - 3
src/views/AIRobotChat/src/components/AutoResizeTextarea.vue

@@ -11,14 +11,14 @@ watch(
   }
 )
 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) {
@@ -29,6 +29,17 @@ const resize = () => {
     el.style.height = maxHeight + 'px'
   }
 }
+
+// 处理中文输入法
+const handleCompositionStart = () => {
+  isComposing.value = true
+}
+
+const handleCompositionEnd = () => {
+  isComposing.value = false
+  nextTick(resize)
+}
+
 const emit = defineEmits(['focus', 'blur'])
 </script>
 
@@ -39,7 +50,9 @@ const emit = defineEmits(['focus', 'blur'])
     class="input-area"
     rows="1"
     :placeholder="props.placeholder"
-    @input="resize"
+    @input="!isComposing && resize()"
+    @compositionstart="handleCompositionStart"
+    @compositionend="handleCompositionEnd"
     @focus="emit('focus')"
     @blur="emit('blur')"
   />

+ 115 - 50
src/views/Booking/src/BookingView.vue

@@ -388,71 +388,90 @@ const SearchInput = () => {
   sessionStorage.setItem('searchTableQeury', JSON.stringify(searchTableQeury))
   getbookingdata()
 }
+
+import BookingGuide from './components/BookingGuide.vue'
+import { useGuideStore } from '@/stores/modules/guide'
+
+const guideStore = useGuideStore()
+const bookingGuideRef = ref()
+const handleGuide = () => {
+  bookingGuideRef.value.startGuide() // 开始引导
+}
 </script>
 
 <template>
-  <div class="Title">Booking</div>
+  <div class="Title">
+    <span>Booking</span>
+    <VDriverGuide @click="handleGuide"></VDriverGuide>
+  </div>
+  <BookingGuide ref="bookingGuideRef"></BookingGuide>
   <div class="display" ref="filterRef">
-    <FilterTags :TagsListItem="TagsList" @changeTag="changeTag"></FilterTags>
-    <div class="heaer_top">
-      <div class="search">
-        <el-input
-          placeholder="Enter Booking/HBL/PO/Carrier Booking No. "
-          v-model="BookingSearch"
-          class="log_input"
-          @keyup.enter="SearchInput"
-        >
-          <template #prefix>
-            <span class="iconfont_icon">
-              <svg class="iconfont icon_search" aria-hidden="true">
-                <use xlink:href="#icon-icon_search_b"></use>
-              </svg>
-            </span>
-          </template>
-          <template #suffix>
-            <el-tooltip
-              v-if="isShowAlertIcon"
-              :offset="6"
-              popper-class="ShowAlerIcon"
-              effect="dark"
-              content="We support the following references number to find bookings:· Booking No./HAWB No./MAWB No./PO No./Carrier Booking No./Contract No./File No./Quote No."
-              placement="bottom"
+    <div class="filter-box">
+      <div class="filters-container" id="booking-filters-container-guide">
+        <FilterTags :TagsListItem="TagsList" @changeTag="changeTag"></FilterTags>
+        <div class="heaer_top">
+          <div class="search">
+            <el-input
+              placeholder="Enter Booking/HBL/PO/Carrier Booking No. "
+              v-model="BookingSearch"
+              class="log_input"
+              @keyup.enter="SearchInput"
             >
-              <span class="iconfont_icon iconfont_icon_tip">
-                <svg class="iconfont icon_search" aria-hidden="true">
-                  <use xlink:href="#icon-icon_info_b"></use>
-                </svg>
-              </span>
-            </el-tooltip>
-          </template>
-        </el-input>
+              <template #prefix>
+                <span class="iconfont_icon">
+                  <svg class="iconfont icon_search" aria-hidden="true">
+                    <use xlink:href="#icon-icon_search_b"></use>
+                  </svg>
+                </span>
+              </template>
+              <template #suffix>
+                <el-tooltip
+                  v-if="isShowAlertIcon"
+                  :offset="6"
+                  popper-class="ShowAlerIcon"
+                  effect="dark"
+                  content="We support the following references number to find bookings:· Booking No./HAWB No./MAWB No./PO No./Carrier Booking No./Contract No./File No./Quote No."
+                  placement="bottom"
+                >
+                  <span class="iconfont_icon iconfont_icon_tip">
+                    <svg class="iconfont icon_search" aria-hidden="true">
+                      <use xlink:href="#icon-icon_info_b"></use>
+                    </svg>
+                  </span>
+                </el-tooltip>
+              </template>
+            </el-input>
+          </div>
+          <TransportMode
+            :isShipment="false"
+            :TransportListItem="TransportListItem"
+            @TransportSearch="TransportSearch"
+            @defaultTransport="defaultTransport"
+            @clearTransportTags="clearTransportTags"
+          ></TransportMode>
+          <DateRange
+            :isShipment="false"
+            @DateRangeSearch="DateRangeSearch"
+            @clearDaterangeTags="clearDaterangeTags"
+            @defaultDate="defaultDate"
+          ></DateRange>
+        </div>
       </div>
-      <TransportMode
-        :isShipment="false"
-        :TransportListItem="TransportListItem"
-        @TransportSearch="TransportSearch"
-        @defaultTransport="defaultTransport"
-        @clearTransportTags="clearTransportTags"
-      ></TransportMode>
-      <DateRange
-        :isShipment="false"
-        @DateRangeSearch="DateRangeSearch"
-        @clearDaterangeTags="clearDaterangeTags"
-        @defaultDate="defaultDate"
-      ></DateRange>
       <MoreFilters
         :isShipment="false"
+        :pageMode="'booking'"
         :searchTableQeury="searchTableQeury"
         @MoreFiltersSearch="MoreFiltersSearch"
         @clearMoreFiltersTags="clearMoreFiltersTags"
         @defaultMorefilters="defaultMorefilters"
+        :isShowMoreFiltersGuidePhoto="guideStore.booking.isShowMoreFiltersGuidePhoto"
       ></MoreFilters>
       <el-button class="el-button--dark" style="margin-left: 8px" @click="SearchInput"
         >Search</el-button
       >
     </div>
     <!-- 筛选项 -->
-    <div class="filtersTag" v-if="tagsData.length">
+    <div class="filtersTag" v-if="tagsData.length" id="booking-filter-tag-guide">
       <el-tag
         :key="tag"
         class="tag"
@@ -484,6 +503,51 @@ const SearchInput = () => {
 </template>
 
 <style lang="scss" scoped>
+.filter-box {
+  width: 100%;
+  display: flex;
+  flex-direction: row;
+  align-items: flex-end;
+  height: 100%;
+}
+.filters-container {
+  max-width: 1426px;
+  width: 80%;
+  display: flex;
+  flex-direction: column;
+}
+.filter-guide-class {
+  top: -3px;
+  left: -2px;
+  height: 29px;
+  width: 592px;
+}
+img.more-filters-guide-class {
+  right: 38px;
+  top: 155px;
+  height: 634px;
+  width: 243px;
+  z-index: 20000;
+}
+.download-file-guide-class {
+  right: 85px;
+  top: 243px;
+  width: 377px;
+  height: 236px;
+}
+.customize-columns-guide-class {
+  right: 8px;
+  top: 249px;
+  width: 694px;
+  height: 474px;
+}
+.tab-filter-guide-class {
+  left: 248px;
+  top: 118px;
+  height: 42px;
+  z-index: 20000;
+}
+
 .Title {
   display: flex;
   height: 68px;
@@ -500,7 +564,6 @@ const SearchInput = () => {
 }
 .heaer_top {
   margin-top: 6.57px;
-  margin-bottom: 8px;
   display: flex;
 }
 .search {
@@ -508,8 +571,9 @@ const SearchInput = () => {
   height: 32px;
 }
 .filtersTag {
-  margin-bottom: 8.7px;
-  display: flex;
+  margin-top: 8px;
+  margin-bottom: 4px;
+  display: inline-flex;
   align-items: center;
   flex-wrap: wrap;
 }
@@ -519,6 +583,7 @@ const SearchInput = () => {
   color: var(--color-neutral-1);
   font-weight: 600;
   font-size: var(--font-size-2);
+  border-color: var(--tag-boder-color);
   background-color: var(--tag-bg-color) !important;
 }
 .iconfont_icon_tip {

+ 243 - 0
src/views/Booking/src/components/BookingGuide.vue

@@ -0,0 +1,243 @@
+<script setup lang="ts">
+import { useDriver } from '@/utils/driverGuide'
+import { useGuideStore } from '@/stores/modules/guide'
+import { useThemeStore } from '@/stores/modules/theme'
+
+import customizeColumnsImgLight from '../image/customize-columns.png'
+import customizeColumnsImgDark from '../image/dark-customize-columns.png'
+
+import downloadFileImgLight from '@/views/Tracking/src/image/download-guide.png'
+import downloadFileImgDark from '@/views/Tracking/src/image/dark-download-guide.png'
+
+const themeStore = useThemeStore()
+
+const customizeColumnsImg = computed(() => {
+  return themeStore.theme === 'dark' ? customizeColumnsImgDark : customizeColumnsImgLight
+})
+const downloadFileImg = computed(() => {
+  return themeStore.theme === 'dark' ? downloadFileImgDark : downloadFileImgLight
+})
+
+const guideStore = useGuideStore()
+const bookingGuideStore = guideStore.booking
+
+const steps: any = [
+  {
+    element: '#booking-filters-container-guide',
+    popover: {
+      title: '',
+      description: `Frequently Used Search Criteria Display Area:
+        <ul>
+          <li>Key Booking Status</li>
+          <li>Reference Numbers</li>
+          <li>Transport Mode</li>
+          <li>Date Type & Range</li>
+        </ul>`,
+      side: 'bottom'
+    }
+  },
+  {
+    element: '#booking-filter-tag-guide',
+    popover: {
+      title: '',
+      description: `
+        <ul>
+          <li>Selected query criteria display area</li>
+          <li>You can quickly clear selected conditions at this position</li>
+        </ul>
+      `,
+      side: 'bottom'
+    }
+  },
+  {
+    element: '#more-filters-guide',
+    popover: {
+      title: '',
+      description: 'Click "More Filters" to see more search options.',
+      side: 'left'
+    }
+  },
+  {
+    element: '.booking-no-header',
+    popover: {
+      title: '',
+      description:
+        'Click on the Booking No. or double-click anywhere on a single booking data to enter the detailed page.',
+      side: 'bottom'
+    }
+  },
+  {
+    element: '#booking-download-file-guide',
+    popover: {
+      title: '',
+      description: `
+        <ul>
+          <li>View the number of shipments selected for download</li>
+          <li>Two download list templates are available</li>
+        </ul>
+      `,
+      side: 'bottom'
+    }
+  },
+  {
+    element: '#booking-customize-columns-guide',
+    popover: {
+      title: '',
+      description: `
+        <ul>
+          <li>Drag to right to add columns</li>
+          <li>Drag to left to unselect columns</li>
+          <li>Drag up and down to reorder or select/unselect</li>
+        </ul>
+      `,
+      side: 'bottom'
+    }
+  },
+  {
+    element: '#page-guide-btn-guide',
+    popover: {
+      title: '',
+      description:
+        'After closing, you can still click the "Page Guide" button to view the page guide of the current page.',
+      side: 'bottom'
+    }
+  }
+]
+
+const guideTimer = ref<ReturnType<typeof setTimeout> | null>(null)
+const { start, moveNext, movePrevious, destroy, hasNextStep, moveTo } = useDriver(steps, {
+  onNextClick: (element, step, options) => {
+    if (options?.state?.activeIndex === 1) {
+      bookingGuideStore.isShowMoreFiltersGuidePhoto = true
+    } else if (options?.state?.activeIndex === 2) {
+      bookingGuideStore.isShowMoreFiltersGuidePhoto = false
+    } else if (options?.state?.activeIndex === 3) {
+      bookingGuideStore.isShowDownloadFileGuidePhoto = true
+    } else if (options?.state?.activeIndex === 4) {
+      bookingGuideStore.isShowDownloadFileGuidePhoto = false
+
+      bookingGuideStore.isShowCustomizeColumnsGuidePhoto = true
+    } else if (options?.state?.activeIndex === 5) {
+      bookingGuideStore.isShowCustomizeColumnsGuidePhoto = false
+    }
+    nextTick(() => {
+      moveNext() // 执行下一步
+    })
+  },
+  onPrevClick: (element, step, options) => {
+    if (options?.state?.activeIndex === 2) {
+      bookingGuideStore.isShowMoreFiltersGuidePhoto = false
+    } else if (options?.state?.activeIndex === 3) {
+      bookingGuideStore.isShowMoreFiltersGuidePhoto = true
+    } else if (options?.state?.activeIndex === 4) {
+      bookingGuideStore.isShowDownloadFileGuidePhoto = false
+    } else if (options?.state?.activeIndex === 5) {
+      bookingGuideStore.isShowDownloadFileGuidePhoto = true
+
+      bookingGuideStore.isShowCustomizeColumnsGuidePhoto = false
+    } else if (options?.state?.activeIndex === 6) {
+      bookingGuideStore.isShowCustomizeColumnsGuidePhoto = true
+    }
+    if (guideTimer.value) {
+      clearTimeout(guideTimer.value)
+      guideTimer.value = null
+    }
+    nextTick(() => {
+      movePrevious() // 执行上一步
+    })
+  },
+  onHighlightStarted: () => {
+    if (!hasNextStep()) {
+      guideTimer.value = setTimeout(() => {
+        destroy()
+      }, 3000)
+    }
+  },
+  onDestroyStarted: (element, step, options) => {
+    bookingGuideStore.isShowFilterGuidePhoto = false
+    bookingGuideStore.isShowMoreFiltersGuidePhoto = false
+    bookingGuideStore.isShowDownloadFileGuidePhoto = false
+    bookingGuideStore.isShowCustomizeColumnsGuidePhoto = false
+    if (hasNextStep()) {
+      moveTo(options.config.steps.length - 1)
+      return
+    }
+    nextTick(() => {
+      destroy() // 销毁导览
+    })
+  },
+  onDestroyed: () => {
+    bookingGuideStore.isShowFilterGuidePhoto = false
+    bookingGuideStore.isShowMoreFiltersGuidePhoto = false
+    bookingGuideStore.isShowDownloadFileGuidePhoto = false
+    bookingGuideStore.isShowCustomizeColumnsGuidePhoto = false
+    if (guideTimer.value) {
+      clearTimeout(guideTimer.value)
+      guideTimer.value = null
+    }
+  }
+})
+const startGuide = () => {
+  bookingGuideStore.isShowFilterGuidePhoto = true // 设置状态
+  nextTick(() => {
+    start() // 开始引导
+  }) // 延时1秒开始引导,确保图片加载完成
+}
+
+defineExpose({
+  startGuide
+})
+</script>
+
+<template>
+  <div>
+    <!-- download-file-guide -->
+    <Teleport to="body">
+      <img
+        :class="{ 'download-file-guide-dark-class': themeStore.theme === 'dark' }"
+        id="booking-download-file-guide"
+        v-show="bookingGuideStore.isShowDownloadFileGuidePhoto"
+        class="position-absolute-guide download-file-guide-class"
+        :src="downloadFileImg"
+        alt=""
+      />
+    </Teleport>
+    <!-- customize-columns-guide  -->
+    <Teleport to="body">
+      <img
+        :class="{ 'customize-columns-guide-dark-class': themeStore.theme === 'dark' }"
+        id="booking-customize-columns-guide"
+        v-show="bookingGuideStore.isShowCustomizeColumnsGuidePhoto"
+        class="position-absolute-guide customize-columns-guide-class"
+        :src="customizeColumnsImg"
+        alt=""
+      />
+    </Teleport>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.position-absolute-guide {
+  position: absolute;
+  z-index: 1200;
+}
+
+.download-file-guide-class {
+  right: 187px;
+  top: 246px;
+  width: 431px;
+  height: 304px;
+  &.download-file-guide-dark-class {
+    right: 187px;
+    width: 431px;
+    height: 304px;
+  }
+}
+.customize-columns-guide-class {
+  right: 5px;
+  top: 245px;
+  width: 682px;
+  height: 473px;
+  transform: translateX(0.9px);
+}
+</style>

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

@@ -70,6 +70,12 @@ const handleColumns = (columns: any, status?: string) => {
         formatter: ({ cellValue }: any) => formatNumber(Number(cellValue), item?.digits)
       }
     }
+    if (item.title === 'Booking No.') {
+      curColumn = {
+        ...curColumn,
+        headerClassName: 'booking-no-header'
+      }
+    }
     return curColumn
   })
   return newColumns
@@ -473,12 +479,20 @@ defineExpose({
     <div class="table-tools">
       <div class="left-total-records">{{ selectedNumber }} Selected</div>
       <div class="right-tools-btn">
-        <el-button class="el-button--main el-button--pain-theme" @click="handleDownload">
-          <span style="margin-right: 8px" class="font_family icon-icon_download_b"></span>
+        <el-button
+          :class="{ 'el-button--pain-theme': themeStore.theme === 'dark' }"
+          class="el-button--main el-button--pain-theme"
+          @click="handleDownload"
+          :style="{
+            paddingRight: themeStore.theme === 'dark' ? '13px' : '16px',
+            paddingLeft: themeStore.theme === 'dark' ? '13px' : '11px'
+          }"
+        >
+          <span style="margin-right: 7px" class="font_family icon-icon_download_b"></span>
           Download
         </el-button>
-        <el-button type="default" @click="handleCustomizeColumns">
-          <span style="margin-right: 8px" class="font_family icon-icon_column_b"></span>
+        <el-button style="padding: 0 17px 0 9px" type="default" @click="handleCustomizeColumns">
+          <span style="margin-right: 6px" class="font_family icon-icon_column_b"></span>
           Customize Columns
         </el-button>
       </div>

BIN
src/views/Booking/src/image/customize-columns.png


BIN
src/views/Booking/src/image/dark-customize-columns.png


+ 53 - 45
src/views/ChatLog/src/ChatLog.vue

@@ -1,21 +1,20 @@
 <script lang="ts" setup>
 import { useCalculatingHeight } from '@/hooks/calculatingHeight'
 import TableView from './components/TableView'
-import LogDialog from '@/views/AIApiLog/src/components/LogDialog.vue'
+import dayjs from 'dayjs'
 
-const OperationSearch = ref()
 const filterRef: Ref<HTMLElement | null> = ref(null)
 const containerHeight = useCalculatingHeight(document.documentElement, 290, [filterRef])
 const searchData = ref({
-  inputModel: '',
-  startDate: '',
-  endDate: '',
-  userType: '',
-  questionType: '',
-  answerType: '',
-  answerSatisfaction: '',
-  comparator: 'thanOrEqual',
-  responseDuration: 0
+  text_search: '',
+  question_date_start: '',
+  question_date_end: '',
+  user_type: '',
+  question_type: '',
+  answer_type: '',
+  answer_satisfication: '',
+  response_duration_type: '',
+  response_duration_num: null
 })
 
 const userTypeList = [
@@ -31,47 +30,47 @@ const userTypeList = [
 const questionTypeList = [
   {
     label: 'Predefined Question',
-    value: 'predefinedQuestion'
+    value: 'Predefined Question'
   },
   {
-    label: 'free text',
-    value: 'freeText'
+    label: 'Free Text',
+    value: 'Free Text'
   }
 ]
 const AnswerTypeList = [
   {
     label: 'Suspend',
-    value: 'suspend'
+    value: 'Suspend'
   },
   {
     label: 'Timeout',
-    value: 'timeout'
+    value: 'Timeout'
   },
   {
     label: 'Predefined Template',
-    value: 'predefinedTemplate'
+    value: 'Predefined Template'
   },
   {
     label: 'AI Answer',
-    value: 'AIAnswer'
+    value: 'AI Answer'
   }
 ]
 const answerSatisfactionList = [
   {
     label: 'Null',
-    value: 'null'
+    value: 'Null'
   },
   {
     label: 'Good',
-    value: 'good'
+    value: 'Good'
   },
   {
     label: 'Not Good',
-    value: 'notGood'
+    value: 'Not Good'
   }
 ]
 
-const comparatorList = [
+const response_duration_typeList = [
   {
     label: '>=',
     value: 'thanOrEqual'
@@ -88,17 +87,17 @@ const comparatorList = [
 const tableRef = ref()
 
 const Search = () => {
-  tableRef.value.SearchOperationLog(searchData.value)
+  tableRef.value.searchTableData(searchData.value)
 }
 const DateChange = (date: any) => {
-  searchData.value.startDate = date[0]
-  searchData.value.endDate = date[1]
-  tableRef.value.SearchOperationLog(searchData.value)
-}
-
-const logDialogRef = ref()
-const openDialog = () => {
-  logDialogRef.value.openDialog()
+  if (!date) {
+    searchData.value.question_date_start = ''
+    searchData.value.question_date_end = ''
+  } else {
+    searchData.value.question_date_start = dayjs(date[0]).format('MM/DD/YYYY')
+    searchData.value.question_date_end = dayjs(date[1]).format('MM/DD/YYYY')
+  }
+  tableRef.value.searchTableData(searchData.value)
 }
 </script>
 <template>
@@ -109,7 +108,8 @@ const openDialog = () => {
         <div class="input-tips_filter">
           <el-input
             placeholder="Search Question ID、User"
-            v-model="OperationSearch"
+            v-model="searchData.text_search"
+            clearable
             class="log_input"
           >
             <template #prefix>
@@ -126,7 +126,7 @@ const openDialog = () => {
           <CalendarDate @DateChange="DateChange"></CalendarDate>
         </div>
         <div class="tips_filter">
-          <el-select v-model="searchData.userType" placeholder="User Type">
+          <el-select v-model="searchData.user_type" clearable placeholder="User Type">
             <el-option
               v-for="item in userTypeList"
               :key="item.value"
@@ -136,7 +136,7 @@ const openDialog = () => {
           </el-select>
         </div>
         <div class="tips_filter">
-          <el-select v-model="searchData.questionType" placeholder="Question Type">
+          <el-select clearable v-model="searchData.question_type" placeholder="Question Type">
             <el-option
               v-for="item in questionTypeList"
               :key="item.value"
@@ -147,7 +147,7 @@ const openDialog = () => {
           </el-select>
         </div>
         <div class="tips_filter">
-          <el-select v-model="searchData.answerType" placeholder="Answer Type">
+          <el-select clearable v-model="searchData.answer_type" placeholder="Answer Type">
             <el-option
               v-for="item in AnswerTypeList"
               :key="item.value"
@@ -158,7 +158,11 @@ const openDialog = () => {
           </el-select>
         </div>
         <div class="tips_filter">
-          <el-select v-model="searchData.answerSatisfaction" placeholder="Answer Satisfaction">
+          <el-select
+            clearable
+            v-model="searchData.answer_satisfication"
+            placeholder="Answer Satisfaction"
+          >
             <el-option
               v-for="item in answerSatisfactionList"
               :key="item.value"
@@ -168,11 +172,16 @@ const openDialog = () => {
             </el-option>
           </el-select>
         </div>
-        <div class="comparator-tips_filter">
+        <div class="response_duration_type-tips_filter">
           <span>Response Duration</span>
-          <el-select v-model="searchData.comparator" style="width: 70px; margin: 0 6px">
+          <el-select
+            clearable
+            v-model="searchData.response_duration_type"
+            style="width: 70px; margin: 0 6px"
+            placeholder=""
+          >
             <el-option
-              v-for="item in comparatorList"
+              v-for="item in response_duration_typeList"
               :key="item.value"
               :label="item.label"
               :value="item.value"
@@ -180,18 +189,17 @@ const openDialog = () => {
             </el-option>
           </el-select>
           <el-input-number
-            v-model="searchData.responseDuration"
-            placeholder="s"
+            v-model="searchData.response_duration_num"
+            placeholder=""
             :controls="false"
             :min="0"
-            style="width: 60px"
+            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>
-    <LogDialog ref="logDialogRef" />
+    <TableView :height="containerHeight" :searchData="searchData" ref="tableRef"></TableView>
   </div>
 </template>
 
@@ -249,7 +257,7 @@ const openDialog = () => {
   height: 32px;
   margin-right: 8px;
 }
-.comparator-tips_filter {
+.response_duration_type-tips_filter {
   flex: 1;
   display: flex;
   align-items: center;

+ 64 - 17
src/views/ChatLog/src/components/TableView/src/TableView.vue

@@ -2,15 +2,20 @@
 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 { autoWidth } from '@/utils/table'
 import { useRowClickStyle } from '@/hooks/rowClickStyle'
 import dayjs from 'dayjs'
 import { formatTimezone, formatNumber } from '@/utils/tools'
+import LogDialog from '@/views/AIApiLog/src/components/LogDialog.vue'
 
 const props = defineProps({
   height: {
     type: Number,
     default: 440
+  },
+  searchdata: {
+    type: Object,
+    default: () => ({})
   }
 })
 
@@ -58,11 +63,12 @@ const handleColumns = (columns: any, status?: string) => {
 // 获取表格列
 const getTableColumns = async () => {
   tableLoadingColumn.value = true
-  await $api.getOperationTableColumns().then((res: any) => {
+  await $api.getChatLogTableColumn().then((res: any) => {
     if (res.code === 200) {
       tableData.value.columns = [
         { type: 'checkbox', width: 50, fixed: 'left' },
-        ...handleColumns(res.data.OperationTableColumns)
+        ...handleColumns(res.data.OperationTableColumns),
+        { title: 'Action', width: 106, fixed: 'right', slots: { default: 'action' } }
       ]
       tableOriginColumnsField.value = res.data.OperationTableColumns
     }
@@ -88,22 +94,21 @@ const assignTableData = (data: any) => {
     allTable.value.data = data.searchData || []
     // 为了让导出的表格列宽度自适应
     nextTick(() => {
-      // allTableRef.value && autoWidth(allTable.value, allTableRef.value)
+      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
-    .SearchOperationLog({
+    .getChatLogTableData({
       cp: pageInfo.value.pageNo,
       ps: pageInfo.value.pageSize,
       rc,
-      ...searchdata
+      ...props.searchdata
     })
     .then((res: any) => {
       if (res.code === 200) {
@@ -114,16 +119,15 @@ const getTableData = async (isPageChange?: boolean) => {
       selectedNumber.value = 0
       selectedTableData.value = []
       nextTick(() => {
-        // tableRef.value && autoWidth(tableData.value, tableRef.value)
+        tableRef.value && autoWidth(tableData.value, tableRef.value)
         tableLoadingTableData.value = false
       })
     })
 }
-const SearchOperationLog = (val: any) => {
-  searchdata = val
+const searchTableData = (val: any) => {
   tableLoadingTableData.value = true
   $api
-    .SearchOperationLog({
+    .getChatLogTableData({
       cp: pageInfo.value.pageNo,
       ps: pageInfo.value.pageSize,
       rc: -1,
@@ -138,7 +142,7 @@ const SearchOperationLog = (val: any) => {
       selectedNumber.value = 0
       selectedTableData.value = []
       nextTick(() => {
-        // tableRef.value && autoWidth(tableData.value, tableRef.value)
+        tableRef.value && autoWidth(tableData.value, tableRef.value)
         tableLoadingTableData.value = false
       })
     })
@@ -146,7 +150,7 @@ const SearchOperationLog = (val: any) => {
 onMounted(() => {
   Promise.all([getTableColumns(), getTableData(false)]).finally(() => {
     nextTick(() => {
-      // tableRef.value && autoWidth(tableData.value, tableRef.value)
+      tableRef.value && autoWidth(tableData.value, tableRef.value)
     })
   })
 })
@@ -274,7 +278,7 @@ const getExportTableData = (status: number) => {
   }
   $api
     .OperationLogDownload({
-      selected_fields: column,
+      // selected_fields: column,
       tmp_search: tempSearch.value
     })
     .then((res: any) => {
@@ -331,8 +335,26 @@ const handleCheckAllChange = ({ records }: any) => {
   selectedNumber.value = records.length
   selectedTableData.value = records
 }
+
+const logDialogRef = ref()
+const logLoading = ref(false)
+const handleLogDetail = (row: any) => {
+  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
+  searchTableData
 })
 </script>
 
@@ -345,7 +367,13 @@ defineExpose({
     element-loading-custom-class="element-loading"
     element-loading-background="rgb(43, 47, 54, 0.7)"
   >
-    <div class="table-tools">
+    <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">
@@ -367,6 +395,20 @@ defineExpose({
       <template #empty v-if="!tableLoadingTableData && tableData.data.length === 0">
         <VEmpty> </VEmpty>
       </template>
+      <!-- action操作栏的插槽 -->
+      <template #action="{ row }">
+        <el-button
+          style="height: 24px; padding: 8px 4px; padding-left: 5px; font-size: 12px"
+          @click="handleLogDetail(row)"
+          v-if="row['Question Type'] !== 'Predefined Question'"
+        >
+          <span
+            style="margin-right: 2px; font-size: 15px"
+            class="font_family icon-icon_ai_api_log_b"
+          ></span
+          >AI API Log
+        </el-button>
+      </template>
     </vxe-grid>
     <vxe-grid :height="10" ref="allTableRef" class="all-table" v-bind="allTable"> </vxe-grid>
     <div class="bottom-pagination">
@@ -385,7 +427,12 @@ defineExpose({
         />
       </div>
     </div>
-    <DownloadDialog @export="getExportTableData" ref="downloadDialogRef" />
+    <DownloadDialog
+      @export="getExportTableData"
+      ref="downloadDialogRef"
+      :isHideSelectColumn="true"
+    />
+    <LogDialog ref="logDialogRef" />
   </div>
 </template>
 

+ 7 - 1
src/views/ChatLog/src/components/TableView/src/components/DownloadDialog.vue

@@ -1,4 +1,10 @@
 <script setup lang="ts">
+const props = withDefaults(
+  defineProps<{
+    isHideSelectColumn: boolean
+  }>(),
+  { isHideSelectColumn: false }
+)
 const dialogVisible = ref(false)
 
 const openDialog = (selectedColumns: string[], slectedDataNumber: number) => {
@@ -41,7 +47,7 @@ defineExpose({
             }}</span>
           </div>
         </div>
-        <div class="download-filter">
+        <div class="download-filter" v-if="!props.isHideSelectColumn">
           <el-radio-group v-model="downloadFilter">
             <el-radio :value="1"
               >Download with selected columns

+ 251 - 21
src/views/Dashboard/src/DashboardView.vue

@@ -21,6 +21,7 @@ const SaveVisible = ref(false)
 interface ManagementItem {
   title: string
   switchValue: boolean
+  isRevenueDisplay: boolean
   text: string
   id: number
   title1: string
@@ -774,15 +775,94 @@ const ClickParams = (val: any) => {
     })
   }
 }
+import kpiChartTipLight from './tipsImage/kpi-chart-tip.png'
+import kpiChartTipDark from './tipsImage/dark-kpi-chart-tip.png'
+import pendingChartTipLight from './tipsImage/pending-chart-tip.png'
+import pendingChartTipDark from './tipsImage/dark-pending-chart-tip.png'
+import etdToEtaChartsTipLight from './tipsImage/etd-to-eta-chart-tip.png'
+import etdToEtaChartsTipDark from './tipsImage/dark-etd-to-eta-chart-tip.png'
+import containerChartTipLight from './tipsImage/container-count-chart-tip.png'
+import containerChartTipDark from './tipsImage/dark-container-count-chart-tip.png'
+import top10ChartTipLight from './tipsImage/top-10-chart-tip.png'
+import top10ChartTipDark from './tipsImage/dark-top-10-chart-tip.png'
+import co2eChartTipLight from './tipsImage/co2e-chart-tip.png'
+import co2eChartTipDark from './tipsImage/dark-co2e-chart-tip.png'
+import revenueSpentChartTipLight from './tipsImage/revenue-spent-chart-tip.png'
+import revenueSpentChartTipDark from './tipsImage/dark-revenue-spent-chart-tip.png'
+import recentStatusChartTipLight from './tipsImage/recent-status-chart-tip.png'
+import recentStatusChartTipDark from './tipsImage/dark-recent-status-chart-tip.png'
+
+const kpiChartTip = computed(() => {
+  return themeStore.theme === 'dark' ? kpiChartTipDark : kpiChartTipLight
+})
+const pendingChartTip = computed(() => {
+  return themeStore.theme === 'dark' ? pendingChartTipDark : pendingChartTipLight
+})
+const etdToEtaChartsTip = computed(() => {
+  return themeStore.theme === 'dark' ? etdToEtaChartsTipDark : etdToEtaChartsTipLight
+})
+const containerChartTip = computed(() => {
+  return themeStore.theme === 'dark' ? containerChartTipDark : containerChartTipLight
+})
+const top10ChartTip = computed(() => {
+  return themeStore.theme === 'dark' ? top10ChartTipDark : top10ChartTipLight
+})
+const co2eChartTip = computed(() => {
+  return themeStore.theme === 'dark' ? co2eChartTipDark : co2eChartTipLight
+})
+const revenueSpentChartTip = computed(() => {
+  return themeStore.theme === 'dark' ? revenueSpentChartTipDark : revenueSpentChartTipLight
+})
+const recentStatusChartTip = computed(() => {
+  return themeStore.theme === 'dark' ? recentStatusChartTipDark : recentStatusChartTipLight
+})
+
+import DashboardGuide from '../src/components/DashboardGuide.vue'
+import { useGuideStore } from '@/stores/modules/guide'
+import { useThemeStore } from '@/stores/modules/theme'
+
+import viewManagementLight from './guideImage/view-management.png'
+import viewManagementDark from './guideImage/dark-view-management.png'
+import saveConfigLight from './guideImage/save-config-guide.png'
+import saveConfigDark from './guideImage/dark-save-config-guide.png'
+import kpiChartLight from './guideImage/kpi-chart-guide.png'
+import kpiChartDark from './guideImage/dark-kpi-chart-guide.png'
+
+const themeStore = useThemeStore()
+
+const viewManagementImg = computed(() => {
+  return themeStore.theme === 'dark' ? viewManagementDark : viewManagementLight
+})
+const saveConfigImg = computed(() => {
+  return themeStore.theme === 'dark' ? saveConfigDark : saveConfigLight
+})
+const kpiChartImg = computed(() => {
+  return themeStore.theme === 'dark' ? kpiChartDark : kpiChartLight
+})
+const guideStore = useGuideStore()
+const handleGuide = () => {
+  dashboardGuideRef.value?.startGuide()
+}
+const dashboardGuideRef = ref(null)
+
+function handleImageClick(event) {
+  event.stopPropagation() // 阻止事件冒泡
+  alert('Image clicked')
+}
 </script>
 <template>
   <div class="dashboard">
     <!-- 评分 -->
     <ScoringSystem></ScoringSystem>
+    <DashboardGuide ref="dashboardGuideRef"></DashboardGuide>
     <!-- Title -->
     <div class="Title">
-      <div>Dashboard</div>
       <div>
+        <span>Dashboard</span>
+        <VDriverGuide style="margin-top: -1px" @click="handleGuide"></VDriverGuide>
+      </div>
+
+      <div style="position: relative">
         <el-popover trigger="click" width="400" popper-style="border-radius: 12px">
           <template #reference>
             <el-button class="el-button--default">
@@ -794,14 +874,16 @@ const ClickParams = (val: any) => {
               View Management
             </el-button>
           </template>
+
           <div class="Management">
             <div class="title">View Management</div>
             <div class="management_content" v-for="(item, index) in Management" :key="index">
               <div class="management_flex">
                 <div class="content_title">{{ item.title }}</div>
-                <div><el-switch v-model="item.switchValue" /></div>
+                <div><el-switch v-model="item.switchValue" :disabled="item.isRevenueDisplay != undefined && item.isRevenueDisplay == false" /></div>
               </div>
               <div class="content_text">{{ item.text }}</div>
+              <div class="content_text_warining" v-if="item.isRevenueDisplay != undefined && item.isRevenueDisplay == false">*To ensure the accuracy of the data display, this report needs to be configured and displayed after communicating clearly with Sales.</div>
             </div>
             <el-divider />
             <div class="tips">
@@ -817,6 +899,17 @@ const ClickParams = (val: any) => {
             </div>
           </div>
         </el-popover>
+
+        <img
+          id="view-management-guide"
+          v-if="guideStore.dashboard.isShowViewManagementGuidePhoto"
+          class="view-management-guide-class"
+          :class="{
+            'view-management-guide-dark-class': themeStore.theme === 'dark'
+          }"
+          :src="viewManagementImg"
+          alt=""
+        />
         <el-popover
           :visible="SaveVisible"
           :popper-style="{
@@ -862,6 +955,18 @@ const ClickParams = (val: any) => {
             <div>Save Layout</div>
           </div>
         </el-popover>
+
+        <!--  -->
+        <img
+          id="save-config-guide"
+          v-if="guideStore.dashboard.isShowSaveConfigGuidePhoto"
+          class="save-config-guide-class position-absolute-guide"
+          :src="saveConfigImg"
+          :class="{
+            'save-config-guide-dark-class': themeStore.theme === 'dark'
+          }"
+          alt=""
+        />
       </div>
     </div>
     <!-- 图表 -->
@@ -884,12 +989,32 @@ const ClickParams = (val: any) => {
         <template v-for="item in Management" :key="item">
           <div v-if="item.title === 'KPI' && item.switchValue" class="filters_left">
             <!-- KPI -->
-            <VBox_Dashboard @changeCancel="changeCancel(item.id)">
+            <VBox_Dashboard
+              style="overflow: visible"
+              @changeCancel="changeCancel(item.id)"
+              :isShowDragIconGudie="true"
+            >
               <template #header>
-                <div class="Title_flex">
-                  {{ item.title }}
+                <div class="Title_flex" style="position: relative">
+                  <img
+                    id="kpi-chart-guide"
+                    v-if="guideStore.dashboard.isShowKpiChartGuidePhoto"
+                    class="kpi-chart-guide-class position-absolute-guide"
+                    :src="kpiChartImg"
+                    alt=""
+                  />
+                  <div>
+                    {{ item.title }}
+                    <VTipTooltip
+                      :img="kpiChartTip"
+                      :width="410"
+                      :label="'KPI Report:Day difference between actual and estimate.'"
+                      placement="bottom-start"
+                    ></VTipTooltip>
+                  </div>
                   <DashFilters
                     :defaultData="KPIDefaulteData"
+                    :isShowTransportModeGuide="true"
                     @FilterSearch="GetKpiData"
                   ></DashFilters>
                   <!-- <el-tooltip
@@ -935,10 +1060,19 @@ const ClickParams = (val: any) => {
             <VBox_Dashboard @changeCancel="changeCancel(item.id)">
               <template #header>
                 <div class="Title_flex">
-                  {{ item.title }}
+                  <div>
+                    {{ item.title }}
+                    <VTipTooltip
+                      :img="pendingChartTip"
+                      :width="420"
+                      :placement="'bottom-start'"
+                      :label="'Pending Report:Showing shipments which are soon to depart/arrive.'"
+                    ></VTipTooltip>
+                  </div>
                   <DashFilters
                     :defaultData="PendingDefaulteData"
                     :radioisDisabled="true"
+                    :img="'./image/kpi-chart-tip.png'"
                     @FilterSearch="GetPendingEcharts"
                   ></DashFilters>
                 </div>
@@ -975,7 +1109,15 @@ const ClickParams = (val: any) => {
             <VBox_Dashboard @changeCancel="changeCancel(item.id)">
               <template #header>
                 <div class="Title_flex">
-                  {{ item.title }}
+                  <div>
+                    {{ item.title }}
+                    <VTipTooltip
+                      :img="etdToEtaChartsTip"
+                      :width="430"
+                      :placement="'bottom-start'"
+                      :label="'ETD to ETA (Days):Distribution of Transit Time (ETA-ETD) for All Shipments in Last 12 Months.'"
+                    ></VTipTooltip>
+                  </div>
                   <DashFilters
                     :defaultData="ETDDefaulteData"
                     @FilterSearch="GetETDEcharts"
@@ -1003,7 +1145,14 @@ const ClickParams = (val: any) => {
             <VBox_Dashboard @changeCancel="changeCancel(item.id)">
               <template #header>
                 <div class="Title_flex">
-                  {{ item.title }}
+                  <div>
+                    {{ item.title }}
+                    <VTipTooltip
+                      :img="containerChartTip"
+                      :placement="'bottom-start'"
+                      :label="'Container Count:Total Container Volume by Month (Last 12 Months)'"
+                    ></VTipTooltip>
+                  </div>
                   <DashFilters
                     :defaultData="ContainerefaultData"
                     @FilterSearch="GetContainerCountEcharts"
@@ -1031,15 +1180,23 @@ const ClickParams = (val: any) => {
             <VBox_Dashboard @changeCancel="changeCancel(item.id)" style="width: 100%">
               <template #header>
                 <div class="Title_flex" style="height: 48px">
-                  <el-tabs
-                    v-model="activeName"
-                    class="demo-tabs"
-                    style="height: 48px"
-                    @tab-click="handleTabClick"
-                  >
-                    <el-tab-pane :label="item.title1" name="first"></el-tab-pane>
-                    <el-tab-pane :label="item.title2" name="second"></el-tab-pane>
-                  </el-tabs>
+                  <div style="display: flex">
+                    <el-tabs
+                      v-model="activeName"
+                      class="demo-tabs"
+                      style="height: 48px"
+                      @tab-click="handleTabClick"
+                    >
+                      <el-tab-pane :label="item.title1" name="first"></el-tab-pane>
+                      <el-tab-pane :label="item.title2" name="second"></el-tab-pane>
+                    </el-tabs>
+                    <VTipTooltip
+                      style="margin-left: 4px"
+                      :img="top10ChartTip"
+                      :label="'Top 10 Origin & Destination: Last 12 Months Shipment Volume Rankings: Top 10 Origin Cities and Top 10 Destination Cities'"
+                      :width="700"
+                    ></VTipTooltip>
+                  </div>
                   <DashFilters
                     :defaultData="Top10DefaultData"
                     @FilterSearch="GetTop10ODEcharts"
@@ -1091,7 +1248,15 @@ const ClickParams = (val: any) => {
             <VBox_Dashboard @changeCancel="changeCancel(item.id)">
               <template #header>
                 <div class="Title_flex">
-                  {{ item.title }}
+                  <div>
+                    {{ item.title }}
+                    <VTipTooltip
+                      :img="co2eChartTip"
+                      :label="'CO2e Emission by Origin or Destination: Last 12 Months CO2e Emission Rankings: Top 10 Origin Cities and Top 10 Destination Cities'"
+                      :width="700"
+                      :placement="'bottom-start'"
+                    ></VTipTooltip>
+                  </div>
                   <DashFilters
                     :defaultData="Co2OriginDefaultData"
                     @FilterSearch="GetCo2EmissionEcharts"
@@ -1119,7 +1284,10 @@ const ClickParams = (val: any) => {
             <VBox_Dashboard @changeCancel="changeCancel(item.id)">
               <template #header>
                 <div class="Title_flex">
-                  {{ item.title }}
+                  <div>
+                    {{ item.title }}
+                    <!-- <VTipTooltip :img="co2eChartTip"></VTipTooltip> -->
+                  </div>
                   <DashFilters
                     :defaultData="Co2DestinationDefaultData"
                     @FilterSearch="GetCo2DestinationEcharts"
@@ -1153,7 +1321,15 @@ const ClickParams = (val: any) => {
             <VBox_Dashboard @changeCancel="changeCancel(item.id)" style="width: 100%">
               <template #header>
                 <div class="Title_flex">
-                  {{ item.title }}
+                  <div>
+                    {{ item.title }}
+                    <VTipTooltip
+                      :img="recentStatusChartTip"
+                      :label="'Recent Status: Active shipment list with ETD within the past three months and the next month.'"
+                      :width="700"
+                      :placement="'bottom-start'"
+                    ></VTipTooltip>
+                  </div>
                   <DashFilters
                     :defaultData="RecentDefaulteData"
                     @FilterSearch="getTableData"
@@ -1184,7 +1360,16 @@ const ClickParams = (val: any) => {
             <VBox_Dashboard @changeCancel="changeCancel(item.id)" style="width: 100%">
               <template #header>
                 <div class="Title_flex">
-                  Revenue Spent
+                  <div>
+                    Revenue Spent
+                    <VTipTooltip
+                      :img="revenueSpentChartTip"
+                      :label="'Revenue Spent: Based on the billto object, display the corresponding revenue data. '"
+                      :placement="'bottom-start'"
+                      :width="700"
+                    ></VTipTooltip>
+                  </div>
+
                   <DashFilters
                     :defaultData="RevenueDefaultData"
                     @FilterSearch="GetRevenueEcharts"
@@ -1224,6 +1409,41 @@ const ClickParams = (val: any) => {
 .iconfont {
   vertical-align: -2px;
 }
+
+.view-management-guide-class {
+  position: absolute;
+  top: 0px;
+  right: 85px;
+  width: 437px;
+  height: 603px;
+  z-index: 1500;
+  &.view-management-guide-dark-class {
+    width: 439px;
+    height: 622px;
+  }
+}
+.save-config-guide-class {
+  position: absolute;
+  top: -1px;
+  right: -13px;
+  width: 183px;
+  height: 160px;
+  z-index: 1500;
+  transform: translate(-0.8px, 0px);
+  &.save-config-guide-dark-class {
+    width: 182px;
+    height: 157px;
+    right: -12px;
+  }
+}
+.kpi-chart-guide-class {
+  top: -2px;
+  left: -50px;
+  width: 589px;
+  height: 478px;
+  z-index: 3500;
+}
+
 .Management {
   max-height: 640px;
   overflow-y: hidden;
@@ -1265,6 +1485,11 @@ const ClickParams = (val: any) => {
   font-size: var(--font-size-2);
   line-height: 16px;
 }
+.content_text_warining {
+  color: var(--color-warning);
+  font-size: var(--font-size-2);
+  line-height: 16px;
+}
 .tips {
   display: flex;
   justify-content: center;
@@ -1412,3 +1637,8 @@ const ClickParams = (val: any) => {
   margin-bottom: 0;
 }
 </style>
+<style lang="scss">
+:not(body):has(> img.driver-active-element) {
+  overflow: visible !important;
+}
+</style>

+ 36 - 4
src/views/Dashboard/src/components/DashFiters.vue

@@ -27,6 +27,10 @@ const props = defineProps({
   isRevenue: {
     type: Boolean,
     default: false
+  },
+  isShowTransportModeGuide: {
+    type: Boolean,
+    default: false
   }
 })
 
@@ -164,6 +168,18 @@ const DateRangeSearch = () => {
   }
   filters_visible.value = false
 }
+
+import { useThemeStore } from '@/stores/modules/theme'
+
+import { useGuideStore } from '@/stores/modules/guide'
+import transportModeLight from '@/views/Dashboard/src/guideImage/transport-mode.png'
+import transportModeDark from '@/views/Dashboard/src/guideImage/dark-transport-mode.png'
+
+const themeStore = useThemeStore()
+const transportModeImg = computed(() => {
+  return themeStore.theme === 'dark' ? transportModeDark : transportModeLight
+})
+const guideStore = useGuideStore()
 </script>
 <template>
   <div class="DashFilters">
@@ -270,13 +286,29 @@ const DateRangeSearch = () => {
         </div>
       </div>
     </el-popover>
+    <img
+      id="transport-mode-guide"
+      :src="transportModeImg"
+      v-if="isShowTransportModeGuide && guideStore.dashboard.isShowTransportModeGuidePhoto"
+      class="transport-mode-guide-class position-absolute-guide"
+      alt=""
+    />
   </div>
 </template>
 <style lang="scss" scoped>
 .DashFilters {
+  position: relative;
   display: flex;
   align-items: center;
 }
+.transport-mode-guide-class {
+  position: absolute;
+  right: -16px;
+  top: -1px;
+  width: 431px;
+  height: 327px;
+  z-index: 1000;
+}
 :deep(.el-checkbox-button__inner) {
   color: var(--tag-info-text-color);
   font-size: var(--font-size-3);
@@ -381,10 +413,10 @@ const DateRangeSearch = () => {
   align-items: center;
 }
 :deep(
-    .el-radio-button.is-active
-      .el-radio-button__original-radio:not(:disabled)
-      + .el-radio-button__inner
-  ) {
+  .el-radio-button.is-active
+    .el-radio-button__original-radio:not(:disabled)
+    + .el-radio-button__inner
+) {
   height: 40px;
   display: flex;
   align-items: center;

+ 156 - 0
src/views/Dashboard/src/components/DashboardGuide.vue

@@ -0,0 +1,156 @@
+<script setup lang="ts">
+import { useDriver } from '@/utils/driverGuide'
+import { useGuideStore } from '@/stores/modules/guide'
+
+const guideStore = useGuideStore()
+
+const dashboardGuideStore = guideStore.dashboard
+const steps: any = [
+  {
+    element: '#view-management-guide',
+    popover: {
+      title: '',
+      description:
+        'The Dashboard integrates different types of reports. You can freely select the reports you need. ',
+      side: 'left'
+    }
+  },
+  {
+    element: '#transport-mode-guide',
+    popover: {
+      title: '',
+      description:
+        'Each report comes with its own filter options. Simply make your selection and click "Search" to refresh the data. ',
+      side: 'bottom'
+    }
+  },
+  {
+    element: '#kpi-chart-guide',
+    popover: {
+      title: '',
+      description:
+        'Hover the icon next to the report name to view the data explanation for each report.',
+      side: 'bottom'
+    }
+  },
+  {
+    element: '#drag-icon-guide',
+    popover: {
+      title: '',
+      description:
+        'Drag and drop reports using the drag handle (eight-dot icon) to reposition them in any direction. ',
+      side: 'bottom'
+    }
+  },
+  {
+    element: '#save-config-guide',
+    popover: {
+      title: '',
+      description: 'Click "Save Layout" to preserve your customized report arrangement.',
+      side: 'bottom'
+    }
+  },
+  {
+    element: '#page-guide-btn-guide',
+    popover: {
+      title: '',
+      description:
+        'After closing, you can still click the "Page Guide" button to view the page guide of the current page.',
+      side: 'bottom'
+    }
+  }
+]
+
+const guideTimer = ref<ReturnType<typeof setTimeout> | null>(null)
+const { start, moveNext, movePrevious, destroy, hasNextStep, moveTo } = useDriver(steps, {
+  onNextClick: (element, step, options) => {
+    if (options?.state?.activeIndex === 0) {
+      dashboardGuideStore.isShowViewManagementGuidePhoto = false
+      dashboardGuideStore.isShowTransportModeGuidePhoto = true
+    } else if (options?.state?.activeIndex === 1) {
+      dashboardGuideStore.isShowTransportModeGuidePhoto = false
+      dashboardGuideStore.isShowKpiChartGuidePhoto = true
+    } else if (options?.state?.activeIndex === 2) {
+      dashboardGuideStore.isShowKpiChartGuidePhoto = false
+    } else if (options?.state?.activeIndex === 3) {
+      dashboardGuideStore.isShowSaveConfigGuidePhoto = true
+    } else if (options?.state?.activeIndex === 4) {
+      dashboardGuideStore.isShowSaveConfigGuidePhoto = false
+    }
+    nextTick(() => {
+      moveNext() // 执行下一步
+    })
+  },
+  onPrevClick: (element, step, options) => {
+    if (options?.state?.activeIndex === 1) {
+      dashboardGuideStore.isShowViewManagementGuidePhoto = true
+      dashboardGuideStore.isShowTransportModeGuidePhoto = false
+    } else if (options?.state?.activeIndex === 2) {
+      dashboardGuideStore.isShowTransportModeGuidePhoto = true
+      dashboardGuideStore.isShowKpiChartGuidePhoto = false
+    } else if (options?.state?.activeIndex === 3) {
+      dashboardGuideStore.isShowKpiChartGuidePhoto = true
+      dashboardGuideStore.isShowSaveConfigGuidePhoto = false
+    } else if (options?.state?.activeIndex === 4) {
+      dashboardGuideStore.isShowSaveConfigGuidePhoto = false
+    } else if (options?.state?.activeIndex === 5) {
+      dashboardGuideStore.isShowSaveConfigGuidePhoto = true
+    }
+    if (guideTimer.value) {
+      clearTimeout(guideTimer.value)
+      guideTimer.value = null
+    }
+    nextTick(() => {
+      movePrevious() // 执行上一步
+    })
+  },
+  onHighlightStarted: () => {
+    if (!hasNextStep()) {
+      guideTimer.value = setTimeout(() => {
+        destroy()
+      }, 3000)
+    }
+  },
+  // 关闭导览时,隐藏所有图片
+  onDestroyStarted: (element, step, options) => {
+    dashboardGuideStore.isShowViewManagementGuidePhoto = false
+    dashboardGuideStore.isShowTransportModeGuidePhoto = false
+    dashboardGuideStore.isShowSaveConfigGuidePhoto = false
+    dashboardGuideStore.isShowKpiChartGuidePhoto = false
+    if (hasNextStep()) {
+      moveTo(options.config.steps.length - 1)
+      return
+    }
+    // showExtraHighlights.value = false // 隐藏额外高亮
+    nextTick(() => {
+      destroy() // 销毁导览
+    }) // 确保在下一次DOM更新后执行销毁
+  },
+  onDestroyed: () => {
+    dashboardGuideStore.isShowViewManagementGuidePhoto = false
+    dashboardGuideStore.isShowTransportModeGuidePhoto = false
+    dashboardGuideStore.isShowSaveConfigGuidePhoto = false
+    dashboardGuideStore.isShowKpiChartGuidePhoto = false
+    if (guideTimer.value) {
+      clearTimeout(guideTimer.value)
+      guideTimer.value = null
+    }
+  }
+})
+const startGuide = () => {
+  dashboardGuideStore.isShowViewManagementGuidePhoto = true
+  nextTick(() => {
+    start() // 开始引导
+  }) // 延时1秒开始引导,确保图片加载完成
+}
+
+defineExpose({
+  startGuide
+})
+</script>
+
+<template>
+  <div></div>
+</template>
+
+<style lang="scss" scoped></style>

BIN
src/views/Dashboard/src/guideImage/co2e-chart-tip.png


BIN
src/views/Dashboard/src/guideImage/container-count-chart-tip.png


BIN
src/views/Dashboard/src/guideImage/dark-kpi-chart-guide.png


BIN
src/views/Dashboard/src/guideImage/dark-save-config-guide.png


BIN
src/views/Dashboard/src/guideImage/dark-transport-mode.png


BIN
src/views/Dashboard/src/guideImage/dark-view-management.png


BIN
src/views/Dashboard/src/guideImage/etd-to-eta-chart-tip.png


BIN
src/views/Dashboard/src/guideImage/kpi-chart-guide.png


BIN
src/views/Dashboard/src/guideImage/kpi-chart-tip.png


BIN
src/views/Dashboard/src/guideImage/pending-chart-tip.png


BIN
src/views/Dashboard/src/guideImage/recent-status-chart-tip.png


BIN
src/views/Dashboard/src/guideImage/revenue-spent-chart-tip.png


BIN
src/views/Dashboard/src/guideImage/save-config-guide.png


BIN
src/views/Dashboard/src/guideImage/top-10-chart-tip.png


BIN
src/views/Dashboard/src/guideImage/transport-mode.png


BIN
src/views/Dashboard/src/guideImage/view-management.png


BIN
src/views/Dashboard/src/tipsImage/co2e-chart-tip.png


BIN
src/views/Dashboard/src/tipsImage/container-count-chart-tip.png


BIN
src/views/Dashboard/src/tipsImage/dark-co2e-chart-tip.png


BIN
src/views/Dashboard/src/tipsImage/dark-container-count-chart-tip.png


BIN
src/views/Dashboard/src/tipsImage/dark-etd-to-eta-chart-tip.png


BIN
src/views/Dashboard/src/tipsImage/dark-kpi-chart-tip.png


BIN
src/views/Dashboard/src/tipsImage/dark-pending-chart-tip.png


BIN
src/views/Dashboard/src/tipsImage/dark-recent-status-chart-tip.png


BIN
src/views/Dashboard/src/tipsImage/dark-revenue-spent-chart-tip.png


BIN
src/views/Dashboard/src/tipsImage/dark-top-10-chart-tip.png


BIN
src/views/Dashboard/src/tipsImage/etd-to-eta-chart-tip.png


BIN
src/views/Dashboard/src/tipsImage/kpi-chart-tip.png


BIN
src/views/Dashboard/src/tipsImage/pending-chart-tip.png


BIN
src/views/Dashboard/src/tipsImage/recent-status-chart-tip.png


BIN
src/views/Dashboard/src/tipsImage/revenue-spent-chart-tip.png


BIN
src/views/Dashboard/src/tipsImage/top-10-chart-tip.png


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

@@ -7,7 +7,9 @@ import ScoringGrade from '@/components/ScoringGrade'
 import AIRobot from '@/components/AIRobot'
 import AIRobotChat from '@/views/AIRobotChat/src/AIRobotChat.vue'
 import LogoMenu from './images/logo_menu.png'
+import { useUserStore } from '@/stores/modules/user'
 
+const userStore = useUserStore()
 const leftAsideWidth = ref('232px')
 const isCollapse = ref(false)
 const handleMenuCollapse = (val: boolean) => {
@@ -36,6 +38,16 @@ const handelClickAIDefault = async (item: any) => {
   }
 }
 
+watch(
+  () => userStore.isLogin,
+  (newVal) => {
+    if (newVal) {
+      getPrompt()
+    } else {
+      AIRobotChatref.value?.clearData()
+    }
+  }
+)
 const getPrompt = () => {
   $api.getPrompt().then((res) => {
     if (res.code === 200) {
@@ -43,6 +55,10 @@ const getPrompt = () => {
     }
   })
 }
+
+const handelclickaiinit = () => {
+  AIRobotChatref.value?.handelclckaiinit()
+}
 onMounted(() => {
   getPrompt()
 })
@@ -75,7 +91,7 @@ onMounted(() => {
         <router-view />
       </el-main>
     </el-container>
-    <AIRobot @AvatarClick="AvatarClick" @handelClickAIDefault="handelClickAIDefault"></AIRobot>
+    <AIRobot @AvatarClick="AvatarClick" @handelClickAIDefault="handelClickAIDefault" @handelclickaiinit=handelclickaiinit></AIRobot>
     <AIRobotChat
       ref="AIRobotChatref"
       v-show="isShowAIRobotChat"

+ 36 - 6
src/views/Layout/src/components/Header/HeaderView.vue

@@ -11,16 +11,18 @@ import { useThemeStore } from '@/stores/modules/theme'
 import { useNotificationMessage } from '@/stores/modules/notificationMessage'
 import NotificationDrawer from './components/NotificationDrawer.vue'
 import TrainingCard from './components/TrainingCard.vue'
+import emitter from '@/utils/bus'
 
 const notificationMsgStore = useNotificationMessage()
+import { useGuideStore } from '@/stores/modules/guide'
+
+const guideStore = useGuideStore()
 const themeStore = useThemeStore()
 const userStore = useUserStore()
 const route = useRoute()
 const router = useRouter()
 const headerSearch = useHeaderSearch()
 
-const trainingCardRef = ref()
-
 // 切换系统主题颜色
 const toggleThemeMode = (theme: string) => {
   themeStore.toggleTheme(theme, true)
@@ -132,6 +134,7 @@ const handleUserManual = () => {
         }
         if (res.data?.code === 403) {
           sessionStorage.clear()
+          emitter.emit('login-out')
           router.push('/login')
           userStore.logout()
           ElMessage.warning({
@@ -194,6 +197,24 @@ onMounted(() => {
     notificationMsgStore.hasUnreadMessages()
   }
 })
+const themePopoverRef = ref()
+// // 点击外部关闭逻辑
+const handleClickOutside = (event) => {
+  const popoverElementRef = themePopoverRef.value?.popperRef?.contentRef
+  if (popoverElementRef && !popoverElementRef.contains(event.target)) {
+    isPopoverVisible.value = false
+  }
+}
+
+const handleShowThemePopover = () => {
+  isPopoverVisible.value = true
+  document.addEventListener('click', handleClickOutside)
+}
+
+const handleCloseThemePopover = () => {
+  isPopoverVisible.value = false
+  document.removeEventListener('click', handleClickOutside)
+}
 </script>
 
 <template>
@@ -204,8 +225,14 @@ onMounted(() => {
     element-loading-custom-class="element-loading"
     element-loading-background="rgb(43, 47, 54, 0.7)"
   >
-    <VBreadcrumb></VBreadcrumb>
-    <TrainingCard ref="trainingCardRef"></TrainingCard>
+    <div style="display: flex">
+      <VBreadcrumb></VBreadcrumb>
+      <TrainingCard ref="trainingCardRef"></TrainingCard>
+      <VDriverGuide
+        v-if="route.name === 'Tracking Detail'"
+        @click="guideStore.handleTrackingDetailGuide()"
+      ></VDriverGuide>
+    </div>
     <div class="right-info">
       <el-input
         v-model="searchValue"
@@ -233,10 +260,12 @@ onMounted(() => {
         placement="bottom-end"
         :width="400"
         trigger="click"
+        themePopoverRef
+        ref="themePopoverRef"
         :visible="isPopoverVisible"
         popper-class="toggle-theme-popover"
-        @show="isPopoverVisible = true"
-        @hide="isPopoverVisible = false"
+        @show="handleShowThemePopover"
+        @hide="handleCloseThemePopover"
       >
         <div>
           <!-- Popover content remains the same -->
@@ -382,6 +411,7 @@ onMounted(() => {
   display: flex;
   justify-content: space-between;
   position: relative;
+  align-items: center;
   height: 100%;
 }
 .right-info {

+ 23 - 7
src/views/Layout/src/components/Header/components/NotificationDrawer.vue

@@ -19,16 +19,25 @@ const notificationTypeList = ref({
 })
 
 const notificationList = ref<any[]>([])
-
+const notificationMessageCardRef = ref()
+const pageInfo = ref({
+  cp: 1,
+  ps: 30
+})
 const getNotificationList = () => {
   loading.value = true
   $api
     .getNotificationList({
-      rules_type: notificationType.value
+      rules_type: notificationType.value,
+      cp: pageInfo.value.cp,
+      ps: pageInfo.value.ps
     })
     .then((res) => {
       if (res.code === 200) {
-        notificationList.value = res.data
+        notificationList.value = [...notificationList.value, ...res.data]
+        if (res.data.length === 0 || res.data.length < pageInfo.value.ps) {
+          notificationMessageCardRef.value.finished = true
+        }
         // nextTick(() => {
         //   init()
         // })
@@ -36,6 +45,10 @@ const getNotificationList = () => {
     })
     .finally(() => {
       loading.value = false
+      pageInfo.value.cp += 1
+      notificationMessageCardRef.value.loading = false
+
+      notificationMessageCardRef.value.adjustScrollTop()
     })
 }
 
@@ -45,7 +58,7 @@ const handleMarkAllRead = () => {
     $api.setMessageRead({ read_type: true }).then((res) => {
       if (res.code === 200) {
         notificationMsgStore.hasUnreadMessages()
-        notificationMessageCardsRef.value.setAllMessageRead()
+        notificationMessageCardRef.value.setAllMessageRead()
       }
     })
   } catch (error) {
@@ -73,9 +86,8 @@ const closeDrawer = () => {
   notificationList.value = []
   notificationType.value = 'all'
   notificationMsgStore.markMessageAsRead()
+  pageInfo.value.cp = 1
 }
-
-const notificationMessageCardsRef = ref()
 </script>
 
 <template>
@@ -131,9 +143,10 @@ const notificationMessageCardsRef = ref()
             @see-all="drawerRef.handleClose()"
             @view-more="drawerRef.handleClose()"
             @jump-tracking="drawerRef.handleClose()"
+            @loading="getNotificationList"
             :data="notificationList"
             :topOffset="185"
-            ref="notificationMessageCardsRef"
+            ref="notificationMessageCardRef"
           />
         </div>
       </el-scrollbar>
@@ -223,6 +236,9 @@ div.layout-toolbar {
     }
   }
 }
+:deep(.footer) {
+  margin-top: 40px;
+}
 </style>
 <style lang="scss">
 div.layout-toolbar {

+ 2 - 2
src/views/Layout/src/components/Menu/MenuView.vue

@@ -171,13 +171,13 @@ const jumpLink = (link: string) => {
       <el-collapse v-model="activeName" accordion>
         <el-collapse-item title="REGIONAL SOLUTIONS" name="1" :icon="CaretRight">
           <div class="blogroll-content">
-            <div class="blogroll-item" @click="jumpLink('https://www.kerrysiamseaport.com/')">
+            <div class="blogroll-item" @click="jumpLink('https://www.ksp.kln.com/')">
               <img
                 style="height: 16px; width: 16px; margin-right: 4px"
                 src="./images/flag.png"
                 alt="string"
               />
-              <span class="title">Kerry Siam Seaport Web Service</span>
+              <span class="title">KLN Siam Seaport Web Service</span>
             </div>
           </div>
         </el-collapse-item>

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

@@ -173,13 +173,14 @@ const handleVerification = () => {
   // 这里是登录逻辑
   // handleLoginAfterVerify()
 }
+const loginLoading = ref(false)
 // 验证结束后通过status值判断调登录还是忘记密码接口
 const confirmVerification = () => {
   // 生成验证成功的密文
   const pwd = dayjs().unix()
   confirmVerifyStatus.value = encryptVerificationPwd(pwd)
   isShowSliderVerification.value = false
-
+  loginLoading.value = true
   if (status.value === 'login') {
     handleLoginAfterVerify()
   } else {
@@ -259,6 +260,9 @@ const handleLoginAfterVerify = () => {
       }
       handleResult(res)
     })
+    .finally(() => {
+      loginLoading.value = false
+    })
 }
 
 // 从忘记密码返回登录
@@ -285,6 +289,7 @@ const handleForgot = () => {
 }
 const handleSendPassword = () => {
   if (!isUserNameExit.value || !loginForm.value.username) {
+    loginLoading.value = false
     return
   }
   // 这里是发送密码逻辑
@@ -299,6 +304,9 @@ const handleSendPassword = () => {
         backLogin(true)
       }
     })
+    .finally(() => {
+      loginLoading.value = false
+    })
 }
 
 const isEmailTips = ref(false)
@@ -327,7 +335,14 @@ const firstLoginTipsRef = ref()
 </script>
 
 <template>
-  <div class="login" :class="{ 'dark-bg': themeStore.theme === 'dark' }">
+  <div
+    class="login"
+    v-loading.fullscreen.lock="loginLoading"
+    element-loading-text="Loading..."
+    element-loading-custom-class="element-loading"
+    element-loading-background="rgb(43, 47, 54, 0.7)"
+    :class="{ 'dark-bg': themeStore.theme === 'dark' }"
+  >
     <ScoringSystem class="scoring-system"></ScoringSystem>
     <el-card class="login-card" v-if="status === 'login'">
       <div class="card-title" :class="{ 'is-dark': themeStore.theme === 'dark' }">

+ 98 - 24
src/views/SystemMessage/src/SystemMessage.vue

@@ -13,19 +13,25 @@ const tabCountList = ref([0, 0, 0, 0, 0])
 const curTabCount = ref([])
 
 const handleCount = (count: number) => {
-  if (!count) return ''
+  if (!count || count < 0) return ''
   return count > 99 ? '99+' : count
 }
+// 计算未读卡片数量
+const unreadCardCount = computed(() => {
+  const curEventNotificationIndex = navList.findIndex((navItem) => {
+    return navItem === activeCardTypeName.value
+  })
+  return tabCountList.value[curEventNotificationIndex] - notificationMsgStore.readCardMap.length
+})
 const handleShowCount = (typeName: string, index: number) => {
   // 在切换type类型时,防止点击的类型值为点击之前类型的值
   if (curTabCount.value?.[index] > -1) {
     return curTabCount.value[index]
   }
-  const count = tabCountList.value[index]
   if (typeName === activeCardTypeName.value) {
-    return handleCount(unreadNotificationList.value.length)
+    return handleCount(unreadCardCount.value)
   }
-  return handleCount(count)
+  return handleCount(tabCountList.value[index])
 }
 
 const navList = [
@@ -52,49 +58,107 @@ const setActiveItem = (item: string) => {
     tabCountList.value.length - 1
   )
 
+  // 滚动到顶部
+  if (notificationMessageCardRef.value) {
+    notificationMessageCardRef.value.scrollContainerRef.scrollTo(0, 0)
+  }
   activeCardTypeName.value = item
   sessionStorage.setItem('activeCardTypeName', item)
   activeTabName.value = 'All Notifications'
-  getNotificationList()
+  getNotificationList(activeTabName.value, true)
 }
 
 const loading = ref(false)
 const notificationList = ref<any[]>([])
 
-const unreadNotificationList = computed(() => {
-  return notificationList.value.filter((item) => !item.info.isRead)
-})
-const readNotificationList = computed(() => {
-  return notificationList.value.filter((item) => item.info.isRead)
+const unreadNotificationList = ref<any[]>([])
+
+const readNotificationList = ref<any[]>([])
+const pageInfo = ref({
+  cp: 0,
+  ps: 30
 })
-const getNotificationList = async () => {
+const getNotificationList = async (
+  tabType: string = 'All Notifications',
+  isChangeNav: boolean = false
+) => {
   loading.value = true
+  if (isChangeNav) {
+    pageInfo.value.cp = 1
+    unreadNotificationList.value = []
+    readNotificationList.value = []
+    notificationList.value = []
+    notificationMessageCardRef.value.adjustScrollTop(0)
+    if (activeCardTypeName.value === 'Feature Update') {
+      pageInfo.value.ps = 100
+    } else {
+      pageInfo.value.ps = 30
+    }
+  } else {
+    pageInfo.value.cp += 1
+  }
   const rulesType = Object.entries(notificationTypeList.value).find(
     (item) => item[1] === activeCardTypeName.value
   )?.[0]
+  let info_type: null | boolean = null
+  if (tabType === 'All Notifications') {
+    info_type = null
+  } else if (tabType === 'Unread') {
+    info_type = true
+  } else {
+    info_type = false
+  }
+
   try {
     await notificationMsgStore.markMessageAsRead()
     $api
       .getSystemMessageData({
-        rules_type: rulesType
+        rules_type: rulesType,
+        cp: pageInfo.value.cp,
+        ps: pageInfo.value.ps,
+        info_type
       })
       .then((res) => {
         if (res.code === 200) {
           const data = res.data
-          notificationList.value = data.cardList
-          tabCountList.value = data.countList
+          const cardList = data?.cardList || []
+
+          // 判断是否结束加载
+          notificationMessageCardRef.value.finished =
+            !cardList.length || cardList.length < pageInfo.value.ps
+
+          const listConfig = {
+            'All Notifications': { list: notificationList, count: tabCountList },
+            Unread: { list: unreadNotificationList },
+            Read: { list: readNotificationList }
+          }
+
+          const currentConfig = listConfig[tabType]
+          if (!currentConfig) return
+
+          if (isChangeNav) {
+            currentConfig.list.value = [...cardList]
+          } else {
+            currentConfig.list.value = [...currentConfig.list.value, ...cardList]
+          }
+
+          if (currentConfig.count) {
+            currentConfig.count.value = data.countList || []
+          }
         }
       })
       .finally(() => {
         loading.value = false
         curTabCount.value = []
+        notificationMessageCardRef.value.loading = false
+        notificationMessageCardRef.value.adjustScrollTop(isChangeNav ? 0 : undefined)
       })
   } catch (error) {
-    console.error(error)
     loading.value = false
     curTabCount.value = []
   }
 }
+const notificationMessageCardRef = ref()
 
 const changeCardRead = () => {
   const readCardMap = notificationMsgStore.readCardMap
@@ -108,12 +172,15 @@ const changeCardRead = () => {
 const activeTabName = ref('All Notifications')
 const handleTabChange = () => {
   // 当前tab页切换时,更新数据
-  const readCardMap = notificationMsgStore.readCardMap
-  notificationList.value.forEach((item) => {
-    if (readCardMap.includes(item.info.id)) {
-      item.info.isRead = true
-    }
-  })
+  // const readCardMap = notificationMsgStore.readCardMap
+  // notificationList.value.forEach((item) => {
+  //   if (readCardMap.includes(item.info.id)) {
+  //     item.info.isRead = true
+  //   }
+  // })
+
+  pageInfo.value.cp = 1
+  getNotificationList(activeTabName.value, true)
 }
 
 onMounted(() => {
@@ -174,10 +241,12 @@ onMounted(() => {
               <NotificationMessageCard
                 v-if="activeTabName === 'All Notifications'"
                 :data="notificationList"
-                @hasCardRead="changeCardRead"
                 :isScrollPadding="true"
                 :isShowInsertionTime="true"
                 :updateReadCardsOnChange="false"
+                @hasCardRead="changeCardRead"
+                @loading="getNotificationList('All Notifications')"
+                ref="notificationMessageCardRef"
               ></NotificationMessageCard>
             </div>
           </el-tab-pane>
@@ -187,9 +256,10 @@ onMounted(() => {
               <div
                 class="count"
                 :style="{ 'padding-top': isMac ? 0 : '1px' }"
-                v-if="unreadNotificationList.length"
+                v-if="unreadNotificationList.length > 0 && unreadCardCount > 0"
               >
-                <span>{{ handleCount(unreadNotificationList.length) }}</span>
+                <!-- handleCount(unreadNotificationList.length) || -->
+                <span>{{ handleCount(unreadCardCount) }}</span>
               </div>
             </template>
             <div style="padding-bottom: 20px" v-if="activeTabName === 'Unread'">
@@ -199,6 +269,8 @@ onMounted(() => {
                 :isScrollPadding="true"
                 :isShowInsertionTime="true"
                 :updateReadCardsOnChange="false"
+                @loading="getNotificationList('Unread')"
+                ref="notificationMessageCardRef"
               ></NotificationMessageCard>
             </div>
           </el-tab-pane>
@@ -211,6 +283,8 @@ onMounted(() => {
                 :isScrollPadding="true"
                 :isShowInsertionTime="true"
                 :data="readNotificationList"
+                @loading="getNotificationList('Read')"
+                ref="notificationMessageCardRef"
               >
               </NotificationMessageCard>
             </div>

+ 72 - 8
src/views/SystemMessage/src/components/SystemMessageDetail.vue

@@ -15,24 +15,64 @@ const notificationData = ref({
 })
 
 const loading = ref(false)
+const finished = ref(false)
 if (route.query.type === 'feature') {
   loading.value = true
 }
+const pageInfo = ref({
+  cp: 0,
+  ps: 30
+})
+const scrollContainerRef = ref<HTMLElement | null>(null)
+const isScrollLoading = ref(false)
+const prevScrollTop = ref(0)
+
+const onScroll = async () => {
+  const el = scrollContainerRef.value
+  if (!el || isScrollLoading.value || finished.value) return
+
+  prevScrollTop.value = el.scrollTop + 50
+
+  const threshold = 50 // 提前50px触发
+  if (el.scrollHeight - el.scrollTop - el.clientHeight <= threshold) {
+    loading.value = true
+    isScrollLoading.value = true
+    await getNotificationList()
+  }
+}
+
 const getNotificationList = async () => {
+  pageInfo.value.cp += 1
   await $api
     .getNotificationDetails({
       rules_type: route.query.rules_type,
       frequency_type: route.query.frequency_type,
-      insert_date_format: route.query.insert_date_format
+      insert_date_format: route.query.insert_date_format,
+      cp: pageInfo.value.cp,
+      ps: pageInfo.value.ps
     })
     .then((res) => {
       if (res.code === 200) {
-        notificationData.value = res.data
+        const data = res.data
+        if (data?.length === 0 || !data) {
+          finished.value = true
+          return
+        }
+        const messageList = notificationData.value.notificationList || []
+        // 合并新数据和旧数据
+        notificationData.value = data
+        notificationData.value.notificationList = [...messageList, ...data.notificationList]
         notificationMsgStore.hasUnreadMessages()
+        // 判断是否结束加载
+        if (!data.notificationList?.length || data.notificationList?.length < pageInfo.value.ps) {
+          finished.value = true
+        }
+        scrollContainerRef.value.scrollTop = prevScrollTop.value
       }
     })
     .finally(() => {
       loading.value = false
+      isScrollLoading.value = false
     })
 }
 
@@ -72,7 +112,7 @@ const handleIframeLoaded = () => {
 
 <template>
   <div class="system-message-detail" v-vloading="loading">
-    <div class="content" v-if="route.query.type !== 'feature'">
+    <div class="content message-list-box" v-if="route.query.type !== 'feature'">
       <div class="header" v-if="notificationData.title">
         <div class="status-icon"></div>
         <div class="title">{{ notificationData.title }}</div>
@@ -100,11 +140,26 @@ const handleIframeLoaded = () => {
           </span>
         </div>
       </div>
-      <EventCard
-        v-for="(item, index) in notificationData.notificationList"
-        :key="index"
-        :data="item"
-      />
+      <div
+        style="height: calc(100% - 70px); overflow: auto"
+        ref="scrollContainerRef"
+        @scroll="onScroll"
+      >
+        <EventCard
+          v-for="(item, index) in notificationData.notificationList"
+          :key="index"
+          :data="item"
+        />
+        <div class="footer">
+          <el-divider style="margin-bottom: 0px" v-if="isScrollLoading"> loading... </el-divider>
+          <el-divider
+            style="margin-bottom: 0px"
+            v-if="finished && notificationData.notificationList.length > 0"
+          >
+            Only display the message data within three months
+          </el-divider>
+        </div>
+      </div>
     </div>
     <div class="content" style="height: 100%" v-else>
       <div class="header" v-if="notificationData.title">
@@ -133,6 +188,11 @@ const handleIframeLoaded = () => {
     margin: auto;
     width: 1000px;
   }
+  .message-list-box {
+    height: 100%;
+    padding: 16px;
+    background-color: var(--color-dialog-body-bg);
+  }
   .notification-card {
     max-width: 800px;
   }
@@ -158,6 +218,10 @@ const handleIframeLoaded = () => {
     line-height: 40px;
     font-size: 12px;
   }
+  .footer {
+    padding-right: 16px;
+    text-align: center;
+  }
   .feature-pdf {
     height: calc(100% - 28px);
   }

+ 87 - 51
src/views/Tracking/src/TrackingView.vue

@@ -634,71 +634,91 @@ const SearchInput = () => {
   sessionStorage.setItem('searchTableQeuryTracking', JSON.stringify(searchTableQeuryTracking))
   Gettrackingdata()
 }
+
+import TrackingGuide from './components/TrackingGuide.vue'
+import { useGuideStore } from '@/stores/modules/guide'
+
+const guideStore = useGuideStore()
+const trackingGuideRef = ref()
+const handleGuide = () => {
+  trackingGuideRef.value.startGuide() // 开始引导
+}
 </script>
 
 <template>
-  <div class="Title">Tracking</div>
+  <div class="Title">
+    <span>Tracking</span>
+    <VDriverGuide @click="handleGuide"></VDriverGuide>
+  </div>
+  <TrackingGuide ref="trackingGuideRef"></TrackingGuide>
   <div class="display" ref="filterRef">
-    <FilterTags :TagsListItem="TagsList" @changeTag="changeTag"></FilterTags>
-    <div class="heaer_top">
-      <div class="search">
-        <el-input
-          placeholder="Enter Booking/HBL/PO/Container/Carrier Booking No. "
-          v-model="TrackingSearch"
-          class="log_input"
-          @keyup.enter="SearchInput"
-        >
-          <template #prefix>
-            <span class="iconfont_icon">
-              <svg class="iconfont icon_search" aria-hidden="true">
-                <use xlink:href="#icon-icon_search_b"></use>
-              </svg>
-            </span>
-          </template>
-          <template #suffix>
-            <el-tooltip
-              v-if="isShowAlertIcon"
-              :offset="6"
-              popper-class="ShowAlerIcon"
-              effect="dark"
-              content="We support the following references number to find bookings:· Booking No./HAWB No./MAWB No./PO No./Carrier Booking No./Contract No./File No./Quote No."
-              placement="bottom"
+    <div class="filter-box">
+      <div class="filters-container" id="filters-container-guide">
+        <FilterTags :TagsListItem="TagsList" @changeTag="changeTag"></FilterTags>
+        <div class="heaer_top">
+          <div class="search">
+            <el-input
+              placeholder="Enter Booking/HBL/PO/Container/Carrier Booking No. "
+              v-model="TrackingSearch"
+              class="log_input"
+              @keyup.enter="SearchInput"
             >
-              <span class="iconfont_icon iconfont_icon_tip">
-                <svg class="iconfont icon_search" aria-hidden="true">
-                  <use xlink:href="#icon-icon_info_b"></use>
-                </svg>
-              </span>
-            </el-tooltip>
-          </template>
-        </el-input>
+              <template #prefix>
+                <span class="iconfont_icon">
+                  <svg class="iconfont icon_search" aria-hidden="true">
+                    <use xlink:href="#icon-icon_search_b"></use>
+                  </svg>
+                </span>
+              </template>
+              <template #suffix>
+                <el-tooltip
+                  v-if="isShowAlertIcon"
+                  :offset="6"
+                  popper-class="ShowAlerIcon"
+                  effect="dark"
+                  content="We support the following references number to find bookings:· Booking No./HAWB No./MAWB No./PO No./Carrier Booking No./Contract No./File No./Quote No."
+                  placement="bottom"
+                >
+                  <span class="iconfont_icon iconfont_icon_tip">
+                    <svg class="iconfont icon_search" aria-hidden="true">
+                      <use xlink:href="#icon-icon_info_b"></use>
+                    </svg>
+                  </span>
+                </el-tooltip>
+              </template>
+            </el-input>
+          </div>
+
+          <TransportMode
+            :isShipment="true"
+            :TransportListItem="TransportListItem"
+            @TransportSearch="TransportSearch"
+            @defaultTransport="defaultTransport"
+            @clearTransportTags="clearTransportTags"
+          ></TransportMode>
+          <DateRange
+            :isShipment="true"
+            @DateRangeSearch="DateRangeSearch"
+            @clearDaterangeTags="clearDaterangeTags"
+            @defaultDate="defaultDate"
+          ></DateRange>
+        </div>
       </div>
-      <TransportMode
-        :isShipment="true"
-        :TransportListItem="TransportListItem"
-        @TransportSearch="TransportSearch"
-        @defaultTransport="defaultTransport"
-        @clearTransportTags="clearTransportTags"
-      ></TransportMode>
-      <DateRange
-        :isShipment="true"
-        @DateRangeSearch="DateRangeSearch"
-        @clearDaterangeTags="clearDaterangeTags"
-        @defaultDate="defaultDate"
-      ></DateRange>
       <MoreFilters
         :isShipment="true"
         :searchTableQeury="searchTableQeuryTracking"
         @MoreFiltersSearch="MoreFiltersSearch"
         @clearMoreFiltersTags="clearMoreFiltersTags"
         @defaultMorefilters="defaultMorefilters"
+        :isShowMoreFiltersGuidePhoto="guideStore.tracking.isShowMoreFiltersGuidePhoto"
+        :pageMode="'tracking'"
       ></MoreFilters>
       <el-button class="el-button--dark" style="margin-left: 8px" @click="SearchInput"
         >Search</el-button
       >
     </div>
     <!-- 筛选项 -->
-    <div class="filtersTag" v-if="tagsData.length">
+    <div class="filtersTag" v-if="tagsData.length" id="filter-tag-guide">
       <el-tag
         :key="tag"
         class="tag"
@@ -730,6 +750,20 @@ const SearchInput = () => {
 </template>
 
 <style lang="scss" scoped>
+.filter-box {
+  width: 100%;
+  display: flex;
+  flex-direction: row;
+  align-items: flex-end;
+  height: 100%;
+}
+.filters-container {
+  max-width: 1426px;
+  width: 80%;
+  display: flex;
+  flex-direction: column;
+}
+
 .Title {
   display: flex;
   height: 68px;
@@ -747,17 +781,18 @@ const SearchInput = () => {
   padding: 0 24px;
 }
 .heaer_top {
+  position: relative;
   margin-top: 6.57px;
-  margin-bottom: 8px;
   display: flex;
 }
 .search {
-  width: 400px;
+  // width: 400px;
   height: 32px;
 }
 .filtersTag {
-  margin-bottom: 8.7px;
-  display: flex;
+  margin-top: 8px;
+  margin-bottom: 4px;
+  display: inline-flex;
   align-items: center;
   flex-wrap: wrap;
 }
@@ -767,6 +802,7 @@ const SearchInput = () => {
   color: var(--color-neutral-1);
   font-weight: 600;
   font-size: var(--font-size-2);
+  border-color: var(--tag-boder-color);
   background-color: var(--tag-bg-color) !important;
 }
 .iconfont_icon_tip {

+ 84 - 0
src/views/Tracking/src/components/MultiHighlightGuide.vue

@@ -0,0 +1,84 @@
+<template>
+  <teleport to="body">
+    <div style="background-color: red">
+      <div v-for="(item, index) in positionedItems" :key="index">
+        <div class="highlight-box" :style="item.boxStyle"></div>
+        <div class="highlight-tip" :style="item.tipStyle">
+          {{ item.tip }}
+        </div>
+      </div>
+    </div>
+  </teleport>
+</template>
+
+<script setup lang="ts">
+import { onMounted, onBeforeUnmount, ref, watch, nextTick } from 'vue'
+
+interface Item {
+  selector: string
+  tip: string
+}
+
+const props = defineProps<{ items: Item[] }>()
+
+const positionedItems = ref<{ boxStyle: any; tipStyle: any; tip: string }[]>([])
+
+const updatePositions = () => {
+  positionedItems.value = props.items.map(({ selector, tip }) => {
+    const el = document.querySelector(selector) as HTMLElement
+    if (!el) return { boxStyle: {}, tipStyle: {}, tip }
+    const rect = el.getBoundingClientRect()
+    return {
+      tip,
+      boxStyle: {
+        position: 'absolute',
+        top: `${rect.top + window.scrollY}px`,
+        left: `${rect.left + window.scrollX}px`,
+        width: `${rect.width}px`,
+        height: `${rect.height}px`,
+        borderRadius: '6px',
+        zIndex: 99998
+      },
+      tipStyle: {
+        position: 'absolute',
+        top: `${rect.top + window.scrollY - 52}px`,
+        left: `${rect.left + window.scrollX}px`,
+        backgroundColor: 'var(--color-tour-popover-bg)',
+        color: 'var(--color-neutral-1)',
+        padding: '16px',
+        borderRadius: '4px',
+        fontSize: '14px',
+        zIndex: 99999,
+        boxShadow: '0 2px 6px rgba(0, 0, 0, 0.2)'
+      }
+    }
+  })
+}
+
+onMounted(() => {
+  nextTick(updatePositions)
+  window.addEventListener('resize', updatePositions)
+  window.addEventListener('scroll', updatePositions)
+})
+
+onBeforeUnmount(() => {
+  window.removeEventListener('resize', updatePositions)
+  window.removeEventListener('scroll', updatePositions)
+})
+
+watch(
+  () => props.items,
+  () => nextTick(updatePositions),
+  { deep: true }
+)
+</script>
+
+<style scoped>
+.highlight-box {
+  pointer-events: none;
+}
+.highlight-tip {
+  pointer-events: none;
+  position: absolute;
+}
+</style>

Alguns ficheiros não foram mostrados porque muitos ficheiros mudaram neste diff