Ver código fonte

feat: 实现ai聊天智能滚动

zhouyuhao 7 meses atrás
pai
commit
9ec159b719

+ 61 - 58
src/components/AIRobot/src/AIRobot.vue

@@ -41,22 +41,28 @@ const DeQuestions = ref([
     isLong: false
   },
   {
-    label: 'List shipments with ETA changes in the last 30 days11111111111111111111111111111111111.',
-    value: 'List shipments with ETA changes in the last 30 days11111111111111111111111111111111111.',
+    label:
+      'List shipments with ETA changes in the last 30 days11111111111111111111111111111111111.',
+    value:
+      'List shipments with ETA changes in the last 30 days11111111111111111111111111111111111.',
     isLong: true
   },
   {
-    label: 'List shipments with ETA changes in the last 30 days1111111111111111111111111111111111122.',
-    value: 'List shipments with ETA changes in the last 30 days1111111111111111111111111111111111122.',
+    label:
+      'List shipments with ETA changes in the last 30 days1111111111111111111111111111111111122.',
+    value:
+      'List shipments with ETA changes in the last 30 days1111111111111111111111111111111111122.',
     isLong: true
   },
   {
-    label: 'List shipments with ETA changes in the last 30 days1111111111111111111111111111111111133.',
-    value: 'List shipments with ETA changes in the last 30 days1111111111111111111111111111111111133.',
+    label:
+      'List shipments with ETA changes in the last 30 days1111111111111111111111111111111111133.',
+    value:
+      'List shipments with ETA changes in the last 30 days1111111111111111111111111111111111133.',
     isLong: true
-  },
+  }
 ])
-const itemGroups = ref([]);
+const itemGroups = ref([])
 
 // 鼠标hover AIRobot图标
 const AvatarMouseEnter = () => {
@@ -93,29 +99,29 @@ const HideAIRobotTopTwo = () => {
 
 // 随机显示方法
 const prepareGroups = () => {
-  const groups = [];
-  let currentGroup = [];
-  let currentHeight = 0;
+  const groups = []
+  let currentGroup = []
+  let currentHeight = 0
+
+  DeQuestions.value.forEach((item) => {
+    const itemHeight = item.isLong ? 2 : 1
 
-  DeQuestions.value.forEach(item => {
-    const itemHeight = item.isLong ? 2 : 1;
-    
     if (currentHeight + itemHeight > 4) {
-      groups.push(currentGroup);
-      currentGroup = [];
-      currentHeight = 0;
+      groups.push(currentGroup)
+      currentGroup = []
+      currentHeight = 0
     }
-    
-    currentGroup.push(item);
-    currentHeight += itemHeight;
-  });
+
+    currentGroup.push(item)
+    currentHeight += itemHeight
+  })
 
   // 添加最后一组
   if (currentGroup.length > 0) {
-    groups.push(currentGroup);
+    groups.push(currentGroup)
   }
 
-  itemGroups.value = groups;
+  itemGroups.value = groups
 }
 
 const isShowLogin = () => {
@@ -123,17 +129,17 @@ const isShowLogin = () => {
   setTimeout(() => {
     isShowDefault.value = false
     isShowAIRobotTop.value = true
-  }, 5000);
+  }, 5000)
 }
 
 onMounted(() => {
   prepareGroups()
-  emitter.on('login-success', isShowLogin);
-});
+  emitter.on('login-success', isShowLogin)
+})
 
 onUnmounted(() => {
-  emitter.off('login-success', isShowLogin);
-});
+  emitter.off('login-success', isShowLogin)
+})
 
 defineExpose({
   isShowLogin
@@ -145,39 +151,34 @@ defineExpose({
     <div class="flex_end" @click="HideAIRobotTop">
       <div class="icon flex_center">
         <span class="iconfont_icon icon_dark">
-            <svg class="iconfont" aria-hidden="true">
-              <use xlink:href="#icon-icon_reject_b"></use>
-            </svg>
-          </span>
+          <svg class="iconfont" aria-hidden="true">
+            <use xlink:href="#icon-icon_reject_b"></use>
+          </svg>
+        </span>
       </div>
     </div>
     <div class="flex_title">
       <div class="AIAvator">
         <img width="40px" src="../image/icon_ai_robot36_b@2x.png" />
       </div>
-      <div class="dialogue_title">
-        Hi! I'm your Freight Assistant, always on call
-      </div>
+      <div 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">
         <div class="dialogue_content_title">
           <div class="dialogue_title_left">
             <img src="../image/icon_faq_b@2x.png" width="24px" />
             Frequently Asked Questions
           </div>
         </div>
-        <el-carousel class="carousel" :autoplay="false" height="190px" style="width: 452px;" >
-          <el-carousel-item 
-            v-for="(group, index) in itemGroups "
-            :key="index"
-          >
+        <el-carousel class="carousel" :autoplay="false" height="190px" style="width: 452px">
+          <el-carousel-item v-for="(group, index) in itemGroups" :key="index">
             <div class="dialogue_container">
-              <div 
+              <div
                 class="dialogue_content_item"
                 v-for="item in group"
                 :key="item.label"
-                :class="{ 'long_item': item.isLong }"
+                :class="{ long_item: item.isLong }"
               >
                 {{ item.label }}
               </div>
@@ -191,25 +192,27 @@ defineExpose({
     <div class="flex_end" @click="HideAIRobotTopTwo">
       <div class="icon flex_center">
         <span class="iconfont_icon icon_dark">
-            <svg class="iconfont" aria-hidden="true">
-              <use xlink:href="#icon-icon_reject_b"></use>
-            </svg>
-          </span>
+          <svg class="iconfont" aria-hidden="true">
+            <use xlink:href="#icon-icon_reject_b"></use>
+          </svg>
+        </span>
       </div>
     </div>
-    <div class="dialogue_title">
-      Hi! I'm your Freight Assistant, always on call
-    </div>
+    <div class="dialogue_title">Hi! I'm your Freight Assistant, always on call</div>
   </div>
   <!-- 悬浮icon -->
   <div class="AIRobot flex_center">
-    <el-popover
-      :visible="visible"
-      placement="top-end"
-      width="auto"
-    >
+    <el-popover :visible="visible" placement="top-end" width="auto">
       <template #reference>
-        <el-avatar @click="AvatarClick" @mouseenter="AvatarMouseEnter" @mouseleave="AvatarMouseLeave" :size="46" shape="square" class="avatar_bg" :src="clickSrc" />
+        <el-avatar
+          @click="AvatarClick"
+          @mouseenter="AvatarMouseEnter"
+          @mouseleave="AvatarMouseLeave"
+          :size="46"
+          shape="square"
+          class="avatar_bg"
+          :src="clickSrc"
+        />
       </template>
       <!-- hover时显示的对话框 -->
       <div v-if="AIRobotHoverVisible" class="AIRobot_dialog">Continue the conversation</div>
@@ -329,10 +332,10 @@ defineExpose({
   overflow: hidden;
   transition: all 0.3s ease;
 }
-.itemLable { 
+.itemLable {
   width: 100%;
 }
 .long_item {
   height: 64px;
 }
-</style>
+</style>

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

@@ -239,7 +239,7 @@ const scrollParentBoxStyle = computed(() => {
   width: 100%;
   overflow-y: auto;
   .scroll-padding {
-    padding: 10px 140px 0px 16px;
+    padding: 0px 140px 0px 16px;
   }
 }
 </style>

+ 3 - 0
src/styles/theme.scss

@@ -305,6 +305,7 @@
   --input-border: #eaebed;
   --color-pause-btn-bg: #fff1e6;
   --color-loading-text: #b5b9bf;
+  --color-warning-tips-bg: #fff4d1;
 }
 
 :root.dark {
@@ -487,4 +488,6 @@
   --input-border: #656f7d;
   --color-pause-btn-bg: #453b36;
   --color-loading-text: #818892;
+  --color-warning-tips-bg: #85681b;
+  --color-warning-tips-text: #edb82f;
 }

+ 175 - 92
src/views/AIRobotChat/src/AIRobotChat.vue

@@ -34,6 +34,11 @@ const robotBubbleImg = computed(() => {
   return themeStore.theme === 'light' ? robotBubbleLight : robotBubbleDark
 })
 
+// 是否显示footer的顶部阴影
+const isShowFooterShadow = ref(false)
+// 是否显示header的底部阴影
+const isShowHeaderShadow = ref(false)
+
 const loadingAnswer = ref(false) // 是否正在加载答案
 interface MessageItem {
   type: 'robot' | 'user'
@@ -41,6 +46,8 @@ interface MessageItem {
   feedback?: 'good' | 'noGood' | '' // 反馈结果
   isShowFeedback?: boolean // 是否展示反馈样式
   isAnswer?: boolean // 是否为用户问题的答案,是则才能展示反馈组件
+  isError?: boolean // 是否为错误消息
+  isCancel?: boolean // 是否为取消消息
 }
 const messages = ref<MessageItem[]>([
   {
@@ -64,6 +71,10 @@ const messages = ref<MessageItem[]>([
     content: 'Of course! Please provide me with the details of your shipment.'
   }
 ])
+messages.value = JSON.parse(sessionStorage.getItem('AIChat')) || messages.value
+onMounted(() => {
+  scrollToBottom()
+})
 
 watch(
   () => messages.value,
@@ -87,22 +98,55 @@ const progressStatus = {
   cancel: 'You have stopped this answer'
 }
 
+const isShowTips = ref(false) // 是否展示提示信息
+
 const progressInterval = ref()
 const handleSend = (question, isPresetQuestion = true) => {
   if (!question) return
 
+  if (loadingAnswer.value) {
+    isShowTips.value = true
+    setTimeout(() => {
+      isShowTips.value = false
+    }, 2000)
+    return
+  }
+  loadingAnswer.value = true
+
   messages.value.push({
     type: 'user',
     content: question
   })
-  !isPresetQuestion ? (question = '') : ''
+  !isPresetQuestion ? (userQuestion.value = '') : ''
   queryTime.value = 0
   messages.value.push({
     type: 'robot',
     content: progressStatus[0]
   })
+  autoScroll.value = true
+  scrollToBottom()
+
   progressInterval.value = setInterval(() => {
     queryTime.value++
+    // 定义时间点与对应状态的映射
+    const timeToStatusMap = {
+      15: 15,
+      30: 30,
+      60: 60
+    }
+    // 获取最后一个消息对象
+    const lastMessage = messages.value[messages.value.length - 1]
+    if (queryTime.value === 120) {
+      clearInterval(progressInterval.value)
+      lastMessage.content = progressStatus[120]
+      lastMessage.isError = true
+      queryTime.value = -1
+      loadingAnswer.value = false
+      return
+    }
+    if (timeToStatusMap[queryTime.value] !== undefined) {
+      lastMessage.content = progressStatus[queryTime.value]
+    }
   }, 1000)
 }
 const showScrollButton = ref(false) // 控制按钮显示
@@ -116,6 +160,12 @@ function handleScroll() {
   autoScroll.value = atBottom
 
   showScrollButton.value = el.scrollHeight > el.clientHeight && !autoScroll.value
+
+  isShowFooterShadow.value =
+    messagesRef.value?.scrollTop <
+    messagesRef.value?.scrollHeight - messagesRef.value?.clientHeight - 1
+
+  isShowHeaderShadow.value = !!messagesRef.value?.scrollTop
 }
 function scrollToBottom() {
   nextTick(() => {
@@ -126,6 +176,7 @@ function scrollToBottom() {
 }
 
 const simulateStreamingMarkdown = () => {
+  loadingAnswer.value = true
   const chunks = [
     '# 欢迎使用 Markdown\n\n',
     '这是一个用于测试的 **Markdown** 文本。\n\n',
@@ -135,10 +186,7 @@ const simulateStreamingMarkdown = () => {
     '- 非常适合逐行流式显示\n\n',
     '这是一段测试!\n'
   ]
-  const clientHeight = messagesRef.value.getBoundingClientRect()
-  console.log(messagesRef.value.offsetHeight, 'messagesRef.value', clientHeight)
 
-  loadingAnswer
   messages.value.push({
     type: 'robot',
     content: '',
@@ -146,12 +194,12 @@ const simulateStreamingMarkdown = () => {
     feedback: '',
     isShowFeedback: false
   })
-  loadingAnswer.value = true
+
   let index = 0
 
-  if (autoScroll.value) {
-    scrollToBottom()
-  }
+  autoScroll.value = true
+  scrollToBottom()
+
   const interval = setInterval(() => {
     index === chunks.length - 1 ? (loadingAnswer.value = false) : ''
     if (index < chunks.length) {
@@ -164,43 +212,14 @@ const simulateStreamingMarkdown = () => {
     } else {
       clearInterval(interval)
     }
-  }, 500) // 每500ms追加一段
-}
-
-// 根据时间更新消息内容
-const updateMessageContent = (time) => {
-  const lastMessageIndex = messages.value.length - 1
-
-  // 确保消息数组不为空
-  if (lastMessageIndex >= 0) {
-    messages.value[lastMessageIndex].content = progressStatus[time]
-    if (time === 120) {
-      clearInterval(progressInterval.value)
-      queryTime.value = -3
-    }
-  }
+  }, 200) // 每500ms追加一段
 }
-watch(
-  () => queryTime.value,
-  (newVal) => {
-    // 定义时间点与对应状态的映射
-    const timeToStatusMap = {
-      15: 15,
-      30: 30,
-      60: 60,
-      120: 120
-    }
 
-    // 如果当前时间点在映射中,更新消息内容
-    if (timeToStatusMap[newVal] !== undefined) {
-      updateMessageContent(timeToStatusMap[newVal])
-    }
-  }
-)
 // 暂停回答
 const handlePause = () => {
   clearInterval(progressInterval.value)
-  queryTime.value = -2
+  queryTime.value = -1
+  messages.value[messages.value.length - 1].isCancel = true
   messages.value[messages.value.length - 1].content = progressStatus.cancel
 }
 
@@ -214,7 +233,7 @@ const handleClose = () => {
 
 <template>
   <div class="ai-robot" :style="{ width: modalSize === 'large' ? '1000px' : '484px' }">
-    <div class="top-section">
+    <div class="top-section" :class="[isShowHeaderShadow ? 'box-shadow' : '']">
       <div class="header">
         <span class="welcome">Hi! I'm your Freight Assistant</span>
         <div class="option-icon">
@@ -232,18 +251,19 @@ const handleClose = () => {
         </div>
       </div>
       <AIQuestions :modalSize="modalSize" @question="handleSend"></AIQuestions>
+      <div class="warning-tips" v-if="isShowTips">
+        <div class="warning-bg">
+          <span class="warning-icon font_family icon-icon_warning_fill_b"></span>
+        </div>
+        <span>Answer in progress, please wait.</span>
+      </div>
     </div>
-    <div
-      class="chat-messages"
-      ref="messagesRef"
-      @scroll="handleScroll"
-      :style="{ marginTop: modalSize === 'large' ? '81px' : '151px' }"
-    >
+    <div class="chat-messages" ref="messagesRef" @scroll="handleScroll">
       <div
         class="message-item"
         :class="[
           msg.type === 'user' ? 'user-bubble' : 'robot-bubble',
-          ((queryTime > -1 && queryTime < 120) || queryTime === -2) && index === messages.length - 1
+          (queryTime > -1 && queryTime < 120 && index === messages.length - 1) || msg.isCancel
             ? 'query-style'
             : ''
         ]"
@@ -252,11 +272,10 @@ const handleClose = () => {
         @mouseenter="msg.isShowFeedback = true"
         @mouseleave="msg.isShowFeedback = false"
       >
-        <!-- 请求失败后的提示icon  -->
-        <span
-          v-if="queryTime === -3 && index === messages.length - 1"
-          class="pause-icon font_family icon-icon_warning_fill_b"
-        ></span>
+        <!-- 请求失败后的提示icon   -->
+        <div class="pause-bg" v-if="msg.isError">
+          <span class="pause-icon font_family icon-icon_warning_fill_b"></span>
+        </div>
         <!-- loading icon -->
         <img
           class="loading-img"
@@ -307,7 +326,7 @@ const handleClose = () => {
         <!-- 暂停回答 icon -->
         <div
           class="pause-btn"
-          v-if="index === messages.length - 1 && queryTime > 30 && queryTime < 120"
+          v-if="index === messages.length - 1 && queryTime > 29 && queryTime < 120"
           @click="handlePause"
         >
           <div class="dot"></div>
@@ -319,20 +338,26 @@ const handleClose = () => {
       </div>
     </div>
 
-    <div class="footer-input" :class="[isFooterInputFocus ? 'focus-style' : '']">
-      <AutoResizeTextarea
-        v-model="userQuestion"
-        :placeholder="'Type your question here...'"
-        @focus="isFooterInputFocus = true"
-        @blur="isFooterInputFocus = false"
-      />
-      <el-button @click="simulateStreamingMarkdown">测试</el-button>
-      <div
-        class="input-icon"
-        :class="[userQuestion ? 'input-style' : 'disable']"
-        @click="handleSend(userQuestion, false)"
-      >
-        <span class="font_family icon-icon_send_b"></span>
+    <div
+      class="footer"
+      :class="[isFooterInputFocus ? 'focus-style' : '', isShowFooterShadow ? 'box-shadow' : '']"
+    >
+      <div class="footer-input">
+        <AutoResizeTextarea
+          style="flex: 1"
+          v-model="userQuestion"
+          :placeholder="'Type your question here...'"
+          @focus="isFooterInputFocus = true"
+          @blur="isFooterInputFocus = false"
+        />
+        <el-button @click="simulateStreamingMarkdown">测试</el-button>
+        <div
+          class="input-icon"
+          :class="[!userQuestion || queryTime !== -1 ? 'disable' : '']"
+          @click="handleSend(userQuestion, false)"
+        >
+          <span class="font_family icon-icon_send_b"></span>
+        </div>
       </div>
     </div>
   </div>
@@ -353,18 +378,23 @@ const handleClose = () => {
   background-color: var(--color-dialog-body-bg);
   overflow: hidden;
   .top-section {
-    height: 150px;
+    position: relative;
+    padding-bottom: 16px;
     background: linear-gradient(
       to bottom,
       var(--color-ai-chat-header-bg-gradient-begin) 10%,
       var(--color-ai-chat-header-bg-gradient-begin) 10%,
       var(--color-ai-chat-header-bg-gradient-end) 100%
     );
+    &.box-shadow {
+      box-shadow: -2px 0px 12px rgba(0, 0, 0, 0.1);
+    }
     .header {
       display: flex;
       justify-content: space-between;
       align-items: center;
       height: 64px;
+      margin-bottom: 6px;
       padding: 0 16px;
       .welcome {
         font-size: 18px;
@@ -383,13 +413,45 @@ const handleClose = () => {
         }
       }
     }
+    .warning-tips {
+      position: absolute;
+      top: 60px;
+      left: 17px;
+      width: calc(100% - 32px);
+      height: 40px;
+      padding-top: 14px;
+      background-color: var(--color-warning-tips-bg);
+      border-radius: 6px;
+      text-align: center;
+      .warning-bg {
+        position: relative;
+        display: inline-flex;
+        align-items: center;
+        justify-content: center;
+        height: 12px;
+        width: 12px;
+        margin-right: 6px;
+        border-radius: 50%;
+        background-color: #fff;
+      }
+      .warning-icon {
+        position: absolute;
+        top: -3px;
+        border-radius: 50%;
+        border: none;
+      }
+      span {
+        color: #edb82f;
+      }
+    }
   }
+
   .chat-messages {
     flex: 1;
     display: flex;
     flex-direction: column;
     gap: 16px;
-    padding: 0 16px 16px;
+    padding: 0 16px 18px;
     overflow: auto;
     .message-item {
       position: relative;
@@ -415,14 +477,21 @@ const handleClose = () => {
           margin-left: 0px;
         }
       }
-
+      .pause-bg {
+        position: relative;
+        display: inline-flex;
+        align-items: center;
+        justify-content: center;
+        height: 12px;
+        width: 12px;
+        margin-right: 6px;
+        border-radius: 50%;
+        background-color: #fff;
+      }
       .pause-icon {
-        height: 16px;
-        width: 16px;
-        margin-top: 1px;
-        margin-right: 4px;
+        position: absolute;
+        top: -2px;
         color: #c9353f;
-        background-color: #fff;
         border-radius: 50%;
         border: none;
       }
@@ -500,18 +569,44 @@ const handleClose = () => {
       position: absolute;
       right: 50%;
       transform: translateX(50%);
-      bottom: 52px;
+      bottom: 58px;
       display: flex;
       justify-content: center;
       align-items: center;
       height: 32px;
       width: 32px;
       border-radius: 50%;
-      background-color: var(--color-customize-column-right-section-bg);
+      background-color: #f5f4f4;
+      box-shadow: 2px 2px 12px 0px rgba(0, 0, 0, 0.1);
+      span {
+        color: #2b2f36;
+      }
       cursor: pointer;
       &:hover {
         background-color: var(--color-theme);
-        color: #fff;
+        span {
+          color: #fff;
+        }
+      }
+    }
+  }
+  .footer {
+    padding: 12px 16px;
+    &.box-shadow {
+      box-shadow: 0px 2px 12px rgba(0, 0, 0, 0.1);
+    }
+    &.focus-style {
+      .footer-input {
+        border: 1px solid var(--color-theme);
+      }
+      .input-icon {
+        background-color: var(--color-theme);
+        span {
+          color: #fff;
+        }
+        &:hover {
+          background-color: #d56200;
+        }
       }
     }
   }
@@ -521,10 +616,10 @@ const handleClose = () => {
     gap: 12px;
     padding: 4px 12px;
     padding-right: 4px;
-    margin: 12px 16px;
     border: 1px solid var(--input-border);
     border-radius: 20px;
     box-sizing: border-box;
+
     .input-icon {
       display: flex;
       justify-content: center;
@@ -538,18 +633,6 @@ const handleClose = () => {
         cursor: not-allowed;
       }
     }
-    .input-style {
-      background-color: var(--color-theme);
-      span {
-        color: #fff;
-      }
-      &:hover {
-        background-color: #d56200;
-      }
-    }
-    &.focus-style {
-      border: 1px solid var(--color-theme);
-    }
   }
 
   @keyframes loading-rotate {

+ 1 - 1
src/views/AIRobotChat/src/components/AIQuestions.vue

@@ -228,7 +228,7 @@ const clickQuestion = (question) => {
 }
 .dialogue_title_left_img {
   position: absolute;
-  top: -22px;
+  top: -20px;
 }
 .dialogue_title_left_text {
   background: var(--color-dialogue_title);

+ 7 - 2
src/views/SystemMessage/src/SystemMessage.vue

@@ -192,7 +192,7 @@ onMounted(() => {
                 <span>{{ handleCount(unreadNotificationList.length) }}</span>
               </div>
             </template>
-            <div style="padding: 10px 140px 0px 16px" v-if="activeTabName === 'Unread'">
+            <div style="padding-bottom: 20px" v-if="activeTabName === 'Unread'">
               <NotificationMessageCard
                 v-if="activeTabName === 'Unread'"
                 :data="unreadNotificationList"
@@ -203,7 +203,7 @@ onMounted(() => {
           </el-tab-pane>
           <el-tab-pane label="Read" name="Read">
             <template #label><span style="margin-right: 4px">Read</span> </template>
-            <div style="padding: 10px 140px 0px 16px" v-if="activeTabName === 'Read'">
+            <div style="padding-bottom: 20px" v-if="activeTabName === 'Read'">
               <NotificationMessageCard
                 v-if="activeTabName === 'Read'"
                 :updateReadCardsOnChange="false"
@@ -322,5 +322,10 @@ onMounted(() => {
       height: 100%;
     }
   }
+  :deep {
+    .scroller {
+      padding-top: 10px;
+    }
+  }
 }
 </style>

+ 5 - 5
src/views/Tracking/src/components/TrackingDetail/src/TrackingDetail.vue

@@ -146,11 +146,11 @@ const SubscribeShipments = () => {
 <template>
   <div class="tracking-detail" v-vloading="loading">
     <!-- 分享链接 -->
-    <div class="share-link" @click="openShareDialog">
-      <el-tooltip content="Share" :offset="4">
+    <el-tooltip content="Share" :offset="4" placement="top">
+      <div class="share-link" @click="openShareDialog">
         <span class="font_family icon-icon_share_b share-icon"></span>
-      </el-tooltip>
-    </div>
+      </div>
+    </el-tooltip>
     <div class="header" :class="{ 'is-dark': themeStore.theme === 'dark' }">
       <div class="detail-status">
         <span
@@ -497,7 +497,7 @@ const SubscribeShipments = () => {
   }
   .share-link {
     position: fixed;
-    bottom: 152px;
+    bottom: 248px;
     right: 20px;
     display: flex;
     align-items: center;