Przeglądaj źródła

feat: 实现部分聊天机器人样式

zhouyuhao 7 miesięcy temu
rodzic
commit
1be49206e4

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

@@ -1,9 +1,115 @@
 <script setup lang="ts">
 const modalSize = ref('large')
+
+const inputVModel = ref()
+
+const isFooterInputFocus = ref(false)
+const textareaRef = ref(null)
+// 实现自适应高度(最多 4 行)
+const resize = () => {
+  const el = textareaRef.value
+  if (!el) return
+
+  el.style.height = 'auto' // 先清空旧高度
+  const scrollHeight = el.scrollHeight
+
+  const maxHeight = 100 // 四行时高度
+
+  if (scrollHeight <= maxHeight) {
+    el.style.overflowY = 'hidden'
+    el.style.height = scrollHeight + 'px'
+  } else {
+    el.style.overflowY = 'auto'
+    el.style.height = maxHeight + 'px'
+  }
+}
+
+// 初始挂载和内容变化都触发 resize
+onMounted(() => nextTick(resize))
+const messages = ref([
+  {
+    type: 'robot',
+    content: 'Hello! How can I assist you today?'
+  },
+  {
+    type: 'user',
+    content: 'Hi! I am your Freight Assistant. How can I help you?'
+  },
+  {
+    type: 'robot',
+    content: 'Can you help me with my shipment?'
+  },
+  {
+    type: 'user',
+    content: 'Of course! Please provide me with the details of your shipment.'
+  }
+])
+
+// 用户问题请求时间
+const queryTime = ref(-1)
+// 当前用户问题回复进度
+const progressStatus = {
+  init: 'You can click on Frequently Asked Questions above or type your own question',
+  '0': 'Thinking about your question...',
+  '15': 'Searching for relevant data, please wait...',
+  '30': 'This query is complex and may take more time',
+  '60': 'You may try simplifying your question or selecting a Frequently Asked Question',
+  '120': 'Sorry, the query failed. Please try again later or select a Frequently Asked Question',
+  cancel: 'You have stopped this answer'
+}
+
+const progressInterval = ref()
+const handleSend = () => {
+  queryTime.value = 0
+  messages.value.push({
+    type: 'robot',
+    content: progressStatus[0]
+  })
+  progressInterval.value = setInterval(() => {
+    queryTime.value++
+  }, 1000)
+}
+
+// 根据时间更新消息内容
+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
+    }
+  }
+}
+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
+  messages.value[messages.value.length - 1].content = progressStatus.cancel
+}
 </script>
 
 <template>
-  <div class="ai-robot">
+  <div class="ai-robot" :style="{ width: modalSize === 'large' ? '1000px' : '484px' }">
     <div class="top-section">
       <div class="header">
         <span class="welcome">Hi! I'm your Freight Assistant</span>
@@ -22,6 +128,62 @@ const modalSize = ref('large')
         </div>
       </div>
     </div>
+    <div class="chat-messages" ref="messagesRef">
+      <div
+        class="message-item"
+        :class="[
+          msg.type === 'user' ? 'user-bubble' : 'robot-bubble',
+          ((queryTime > -1 && queryTime < 120) || queryTime === -2) && index === messages.length - 1
+            ? 'query-style'
+            : ''
+        ]"
+        v-for="(msg, index) in messages"
+        :key="index"
+      >
+        <img
+          class="loading-img"
+          v-if="queryTime > -1 && queryTime < 120 && index === messages.length - 1"
+          src="./image/icon_loading.png"
+          alt=""
+        />
+        {{ msg.content }}
+        <img
+          class="robot-bubble-img"
+          v-if="msg.type === 'robot'"
+          src="./image/robotBubbleLight.png"
+          alt=""
+        />
+        <img
+          class="user-bubble-img"
+          v-else-if="msg.type === 'user'"
+          src="./image/userBubbleLight.png"
+          alt=""
+        />
+        <div
+          class="pause-btn"
+          v-if="index === messages.length && queryTime > 30 && queryTime < 120"
+          @click="handlePause"
+        >
+          <div class="dot"></div>
+        </div>
+      </div>
+    </div>
+
+    <div class="footer-input" :class="[isFooterInputFocus ? 'focus-style' : '']">
+      <textarea
+        ref="textareaRef"
+        v-model="inputVModel"
+        class="input-area"
+        rows="1"
+        placeholder="Type your question here..."
+        @input="resize"
+        @focus="isFooterInputFocus = true"
+        @blur="isFooterInputFocus = false"
+      />
+      <div class="input-icon" :class="[inputVModel ? 'input-style' : '']" @click="handleSend">
+        <span class="font_family icon-icon_send_b"></span>
+      </div>
+    </div>
   </div>
 </template>
 
@@ -30,9 +192,10 @@ const modalSize = ref('large')
   position: absolute;
   top: 74px;
   right: 24px;
-  width: 1000px;
   height: calc(100% - 98px);
   z-index: 4000;
+  display: flex;
+  flex-direction: column;
   border-radius: 12px;
   box-shadow: 4px 4px 32px 0px rgba(0, 0, 0, 0.2);
   background-color: #fff;
@@ -64,5 +227,128 @@ const modalSize = ref('large')
       }
     }
   }
+  .chat-messages {
+    flex: 1;
+    display: flex;
+    flex-direction: column;
+    gap: 16px;
+    margin-top: 81px;
+    padding: 0 16px;
+    .message-item {
+      position: relative;
+      display: inline-block;
+      padding: 11px 8px;
+      margin-bottom: 7px;
+      border-radius: 12px;
+      .loading-img {
+        width: 16px;
+        height: 16px;
+        margin-top: -1px;
+        margin-right: 2px;
+        animation: loading-rotate 2s linear infinite;
+      }
+
+      .pause-btn {
+        position: absolute;
+        right: -22px;
+        top: 13px;
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        height: 16px;
+        width: 16px;
+        border-radius: 50%;
+        background-color: #fff1e6;
+        .dot {
+          height: 5px;
+          width: 5px;
+          border-radius: 1px;
+          background-color: var(--color-theme);
+        }
+      }
+    }
+    .query-style {
+      color: #b5b9bf;
+    }
+    .robot-bubble {
+      background: #f2f4f7;
+      align-self: flex-start;
+      .robot-bubble-img {
+        position: absolute;
+        left: -1px;
+        bottom: -7px;
+      }
+    }
+    .user-bubble {
+      align-self: flex-end;
+      background: linear-gradient(to right, #ffede6, #f2f4f7);
+      .user-bubble-img {
+        position: absolute;
+        right: 0;
+        bottom: -7px;
+      }
+    }
+  }
+  .footer-input {
+    display: flex;
+    align-items: flex-end;
+    gap: 12px;
+    padding: 4px 12px;
+    padding-right: 4px;
+    margin: 12px 16px;
+    border: 1px solid #eaebed;
+    border-radius: 20px;
+    box-sizing: border-box;
+    .input-icon {
+      display: flex;
+      justify-content: center;
+      align-items: center;
+      height: 32px;
+      width: 32px;
+      padding: 1px 0 0 2px;
+      border-radius: 50%;
+      cursor: pointer;
+    }
+    .input-style {
+      background-color: var(--color-theme);
+      span {
+        color: #fff;
+      }
+      &:hover {
+        background-color: #d56200;
+      }
+    }
+  }
+  .focus-style {
+    border-color: var(--color-theme);
+  }
+  .input-area {
+    width: 100%;
+    font-size: 14px;
+    line-height: 21px;
+    padding: 4px;
+    resize: none;
+    overflow-y: hidden; // 默认不显示滚动条
+    height: 40px; // 初始高度(1 行)
+    max-height: 100px; // 最多 4 行
+    box-sizing: border-box;
+    border: none;
+    outline-color: #fff;
+    border-radius: 8px;
+    transition: height 0.1s ease;
+    &::placeholder {
+      color: #b5b9bf;
+      opacity: 1;
+    }
+  }
+  @keyframes loading-rotate {
+    0% {
+      transform: rotate(0deg);
+    }
+
+    100% {
+      transform: rotate(360deg);
+    }
+  }
 }
 </style>

BIN
src/views/AIRobotChat/src/image/icon_loading.png


BIN
src/views/AIRobotChat/src/image/robotBubbleLight.png


BIN
src/views/AIRobotChat/src/image/userBubbleLight.png


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

@@ -44,7 +44,7 @@ const handleMenuCollapse = (val: boolean) => {
       </el-main>
     </el-container>
     <!-- <AIRobot></AIRobot> -->
-    <!-- <AIRobotChat></AIRobotChat> -->
+    <AIRobotChat></AIRobotChat>
     <ScoringGrade></ScoringGrade>
   </el-container>
 </template>