Răsfoiți Sursa

【增加】AI Chat 抽离 Message

cherishsince 1 an în urmă
părinte
comite
c3884c5dfd
2 a modificat fișierele cu 297 adăugiri și 174 ștergeri
  1. 269 0
      src/views/ai/chat/Message.vue
  2. 28 174
      src/views/ai/chat/index.vue

+ 269 - 0
src/views/ai/chat/Message.vue

@@ -0,0 +1,269 @@
+<template>
+  <div ref="messageContainer" style="height: 100%;overflow-y: auto;">
+    <div class="chat-list" v-for="(item, index) in list" :key="index" >
+      <!--  靠左 message  -->
+      <!-- TODO 芋艿:类型判断 -->
+      <div class="left-message message-item" v-if="item.type === 'system'">
+        <div class="avatar">
+          <el-avatar
+            src="https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png"
+          />
+        </div>
+        <div class="message">
+          <div>
+            <el-text class="time">{{ formatDate(item.createTime) }}</el-text>
+          </div>
+          <div class="left-text-container" ref="markdownViewRef">
+            <MarkdownView class="left-text" :content="item.content" />
+          </div>
+          <div class="left-btns">
+            <div class="btn-cus" @click="noCopy(item.content)">
+              <img class="btn-image" src="@/assets/ai/copy.svg"/>
+              <el-text class="btn-cus-text">复制</el-text>
+            </div>
+            <div class="btn-cus" style="margin-left: 20px" @click="onDelete(item.id)">
+              <img class="btn-image" src="@/assets/ai/delete.svg" style="height: 17px"/>
+              <el-text class="btn-cus-text">删除</el-text>
+            </div>
+          </div>
+        </div>
+      </div>
+      <!--  靠右 message  -->
+      <div class="right-message message-item" v-if="item.type === 'user'">
+        <div class="avatar">
+          <el-avatar
+            src="https://cube.elemecdn.com/0/88/03b0d39583f48206768a7534e55bcpng.png"
+          />
+        </div>
+        <div class="message">
+          <div>
+            <el-text class="time">{{ formatDate(item.createTime) }}</el-text>
+          </div>
+          <div class="right-text-container">
+            <div class="right-text">{{ item.content }}</div>
+          </div>
+          <div class="right-btns">
+            <div class="btn-cus" @click="noCopy(item.content)">
+              <img class="btn-image" src="@/assets/ai/copy.svg"/>
+              <el-text class="btn-cus-text">复制</el-text>
+            </div>
+            <div class="btn-cus" style="margin-left: 20px" @click="onDelete(item.id)">
+              <img class="btn-image" src="@/assets/ai/delete.svg" style="height: 17px"/>
+              <el-text class="btn-cus-text">删除</el-text>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+<script setup lang="ts">
+import {formatDate} from "@/utils/formatTime";
+import MarkdownView from "@/components/MarkdownView/index.vue";
+import {ChatMessageApi, ChatMessageVO} from "@/api/ai/chat/message";
+import {useClipboard} from "@vueuse/core";
+import {PropType} from "vue";
+
+const {copy} = useClipboard() // 初始化 copy 到粘贴板
+// 判断 消息列表 滚动的位置(用于判断是否需要滚动到消息最下方)
+const messageContainer: any = ref(null)
+const isScrolling = ref(false) //用于判断用户是否在滚动
+
+// 定义 props
+const props = defineProps({
+  list: {
+    type: Array as PropType<ChatMessageVO[]>,
+    required: true
+  }
+})
+
+
+// ============ 处理对话滚动 ==============
+
+const scrollToBottom = async (isIgnore?: boolean) =>{
+   await nextTick(() => {
+    //注意要使用nexttick以免获取不到dom
+    if (isIgnore || !isScrolling.value) {
+      messageContainer.value.scrollTop =
+        messageContainer.value.scrollHeight - messageContainer.value.offsetHeight
+    }
+  })
+}
+
+function handleScroll() {
+  const scrollContainer = messageContainer.value
+  const scrollTop = scrollContainer.scrollTop
+  const scrollHeight = scrollContainer.scrollHeight
+  const offsetHeight = scrollContainer.offsetHeight
+  console.log('scrollTop', scrollTop)
+  if ((scrollTop + offsetHeight) < (scrollHeight - 100)) {
+    // 用户开始滚动并在最底部之上,取消保持在最底部的效果
+    isScrolling.value = true
+  } else {
+    // 用户停止滚动并滚动到最底部,开启保持到最底部的效果
+    isScrolling.value = false
+  }
+}
+
+/**
+ * 复制
+ */
+const noCopy = async (content) => {
+  copy(content)
+  ElMessage({
+    message: '复制成功!',
+    type: 'success'
+  })
+}
+
+/**
+ * 删除
+ */
+const onDelete = async (id) => {
+  // 删除 message
+  await ChatMessageApi.delete(id)
+  ElMessage({
+    message: '删除成功!',
+    type: 'success'
+  })
+  // 回调
+  emits('onDeleteSuccess')
+}
+
+// 监听 list
+const { list, conversationId } = toRefs(props)
+watch(list, async (newValue, oldValue) => {
+  console.log('watch list', list)
+})
+
+// 提供方法给 parent 调用
+defineExpose({scrollToBottom})
+
+//
+const emits = defineEmits(['onDeleteSuccess'])
+//
+onMounted(async () => {
+  messageContainer.value.addEventListener('scroll', handleScroll)
+})
+</script>
+
+<style scoped lang="scss">
+.message-container {
+  position: relative;
+  //top: 0;
+  //bottom: 0;
+  //left: 0;
+  //right: 0;
+  //width: 100%;
+  //height: 100%;
+  overflow-y: scroll;
+  padding: 0 15px;
+  //z-index: -1;
+
+
+
+}
+
+// 中间
+.chat-list {
+  display: flex;
+  flex-direction: column;
+  overflow-y: hidden;
+
+  .message-item {
+    margin-top: 50px;
+  }
+
+  .left-message {
+    display: flex;
+    flex-direction: row;
+  }
+
+  .right-message {
+    display: flex;
+    flex-direction: row-reverse;
+    justify-content: flex-start;
+  }
+
+  .avatar {
+    //height: 170px;
+    //width: 170px;
+  }
+
+  .message {
+    display: flex;
+    flex-direction: column;
+    text-align: left;
+    margin: 0 15px;
+
+    .time {
+      text-align: left;
+      line-height: 30px;
+    }
+
+    .left-text-container {
+      display: flex;
+      flex-direction: column;
+      overflow-wrap: break-word;
+      background-color: rgba(228, 228, 228, 0.8);
+      box-shadow: 0 0 0 1px rgba(228, 228, 228, 0.8);
+      border-radius: 10px;
+      padding: 10px 10px 5px 10px;
+
+      .left-text {
+        color: #393939;
+        font-size: 0.95rem;
+      }
+    }
+
+    .right-text-container {
+      display: flex;
+      flex-direction: row-reverse;
+
+      .right-text {
+        font-size: 0.95rem;
+        color: #fff;
+        display: inline;
+        background-color: #267fff;
+        color: #fff;
+        box-shadow: 0 0 0 1px #267fff;
+        border-radius: 10px;
+        padding: 10px;
+        width: auto;
+        overflow-wrap: break-word;
+      }
+    }
+
+    .left-btns,
+    .right-btns {
+      display: flex;
+      flex-direction: row;
+      margin-top: 8px;
+    }
+  }
+
+  // 复制、删除按钮
+  .btn-cus {
+    display: flex;
+    background-color: transparent;
+    align-items: center;
+
+    .btn-image {
+      height: 20px;
+      margin-right: 5px;
+    }
+
+    .btn-cus-text {
+      color: #757575;
+    }
+  }
+
+  .btn-cus:hover {
+    cursor: pointer;
+  }
+
+  .btn-cus:focus {
+    background-color: #8c939d;
+  }
+}
+</style>

+ 28 - 174
src/views/ai/chat/index.vue

@@ -34,7 +34,7 @@
       <!--  main    -->
       <el-main class="main-container">
         <div class="message-container" >
-          <Message ref="messageRef" :list="list" />
+          <Message ref="messageRef" :list="list" @onDeleteSuccess="handlerMessageDelete" />
         </div>
       </el-main>
       <el-footer class="footer-container">
@@ -82,12 +82,10 @@
 </template>
 
 <script setup lang="ts">
-import MarkdownView from '@/components/MarkdownView/index.vue'
 import Conversation from './Conversation.vue'
 import Message from './Message.vue'
 import {ChatMessageApi, ChatMessageVO} from '@/api/ai/chat/message'
 import {ChatConversationApi, ChatConversationVO} from '@/api/ai/chat/conversation'
-import {formatDate} from '@/utils/formatTime'
 import {useClipboard} from '@vueuse/core'
 import ChatConversationUpdateForm from "@/views/ai/chat/components/ChatConversationUpdateForm.vue";
 
@@ -104,9 +102,7 @@ const inputTimeout = ref<any>() // 处理输入中回车的定时器
 const prompt = ref<string>() // prompt
 
 // 判断 消息列表 滚动的位置(用于判断是否需要滚动到消息最下方)
-const messageContainer: any = ref(null)
 const messageRef = ref()
-const isScrolling = ref(false) //用于判断用户是否在滚动
 const isComposing = ref(false) // 判断用户是否在输入
 
 /** chat message 列表 */
@@ -114,30 +110,10 @@ const list = ref<ChatMessageVO[]>([]) // 列表的数据
 
 // ============ 处理对话滚动 ==============
 
-function scrollToBottom() {
-  // nextTick(() => {
-  //   //注意要使用nexttick以免获取不到dom
-  //   console.log('isScrolling.value', isScrolling.value)
-  //   if (!isScrolling.value) {
-  //     messageContainer.value.scrollTop =
-  //       messageContainer.value.scrollHeight - messageContainer.value.offsetHeight
-  //   }
-  // })
-}
-
-function handleScroll() {
-  const scrollContainer = messageContainer.value
-  const scrollTop = scrollContainer.scrollTop
-  const scrollHeight = scrollContainer.scrollHeight
-  const offsetHeight = scrollContainer.offsetHeight
-  console.log('scrollTop', scrollTop)
-  if ((scrollTop + offsetHeight) < (scrollHeight - 50)) {
-    // 用户开始滚动并在最底部之上,取消保持在最底部的效果
-    isScrolling.value = true
-  } else {
-    // 用户停止滚动并滚动到最底部,开启保持到最底部的效果
-    isScrolling.value = false
-  }
+function scrollToBottom(isIgnore?: boolean) {
+  nextTick(() => {
+    messageRef.value.scrollToBottom(isIgnore)
+  })
 }
 
 // ============= 处理聊天输入回车发送 =============
@@ -203,7 +179,7 @@ const onSend = async () => {
   } as ChatMessageVO
   // list.value.push(userMessage)
   // 滚动到住下面
-  scrollToBottom()
+  await scrollToBottom()
   // stream
   await doSendStream(userMessage)
 }
@@ -221,7 +197,7 @@ const doSendStream = async (userMessage: ChatMessageVO) => {
       userMessage.conversationId, // TODO 芋艿:这里可能要在优化;
       userMessage.content,
       conversationInAbortController.value,
-      (message) => {
+      async (message) => {
         console.log('message', message)
         const data = JSON.parse(message.data) // TODO 芋艿:类型处理;
         // debugger
@@ -246,7 +222,7 @@ const doSendStream = async (userMessage: ChatMessageVO) => {
           list.value[list.value - 1] = lastMessage
         }
         // 滚动到最下面
-        scrollToBottom()
+        await scrollToBottom()
       },
       (error) => {
         console.log('error', error)
@@ -291,34 +267,12 @@ const getMessageList = async () => {
     // 滚动到最下面
     await nextTick(() => {
       // 滚动到最后
-      messageRef.value.scrollToBottom(true)
+      scrollToBottom()
     })
   } finally {
   }
 }
 
-function noCopy(content) {
-  copy(content)
-  ElMessage({
-    message: '复制成功!',
-    type: 'success'
-  })
-}
-
-const onDelete = async (id) => {
-  // 删除 message
-  await ChatMessageApi.delete(id)
-  ElMessage({
-    message: '删除成功!',
-    type: 'success'
-  })
-  // tip:如果 stream 进行中的 message,就需要调用 controller 结束
-  await stopStream()
-  // 重新获取 message 列表
-  await getMessageList()
-}
-
-
 /** 修改聊天会话 */
 const chatConversationUpdateFormRef = ref()
 const openChatConversationUpdateForm = async () => {
@@ -337,13 +291,13 @@ const handlerTitleSuccess = async () => {
  * 对话 - 点击
  */
 const handleConversationClick = async (conversation: ChatConversationVO) => {
-  // 滚动位置复位
-  isScrolling.value = false
   // 更新选中的对话 id
   activeConversationId.value = conversation.id
   activeConversation.value = conversation
   // 刷新 message 列表
   await getMessageList()
+  // 滚动底部
+  scrollToBottom(true)
 }
 
 /**
@@ -373,6 +327,13 @@ const getConversation = async (id: string) => {
   return conversation
 }
 
+// ============ message ===========
+
+const handlerMessageDelete = async () => {
+  // 刷新 message
+  await getMessageList()
+}
+
 /** 初始化 **/
 onMounted(async () => {
   // 设置当前对话 TODO 角色仓库过来的,自带 conversationId 需要选中
@@ -381,10 +342,6 @@ onMounted(async () => {
     activeConversationId.value = id
     activeConversation.value = await getConversation(id) as ChatConversationVO
   }
-  // 获得聊天会话列表
-  // await getChatConversationList()
-  // 获取对话信息
-  // await getConversation(conversationId.value)
   // 获取列表数据
   await getMessageList()
   // scrollToBottom();
@@ -541,120 +498,17 @@ onMounted(async () => {
   margin: 0;
   padding: 0;
   position: relative;
-}
-
-.message-container {
-  position: absolute;
-  top: 0;
-  bottom: 0;
-  left: 0;
-  right: 0;
-  //width: 100%;
-  //height: 100%;
-  overflow-y: scroll;
-  padding: 0 15px;
-}
-
-// 中间
-.chat-list {
-  display: flex;
-  flex-direction: column;
-  overflow-y: hidden;
-
-  .message-item {
-    margin-top: 50px;
-  }
-
-  .left-message {
-    display: flex;
-    flex-direction: row;
-  }
-
-  .right-message {
-    display: flex;
-    flex-direction: row-reverse;
-    justify-content: flex-start;
-  }
-
-  .avatar {
-    //height: 170px;
-    //width: 170px;
-  }
-
-  .message {
-    display: flex;
-    flex-direction: column;
-    text-align: left;
-    margin: 0 15px;
-
-    .time {
-      text-align: left;
-      line-height: 30px;
-    }
-
-    .left-text-container {
-      display: flex;
-      flex-direction: column;
-      overflow-wrap: break-word;
-      background-color: rgba(228, 228, 228, 0.8);
-      box-shadow: 0 0 0 1px rgba(228, 228, 228, 0.8);
-      border-radius: 10px;
-      padding: 10px 10px 5px 10px;
-
-      .left-text {
-        color: #393939;
-        font-size: 0.95rem;
-      }
-    }
-
-    .right-text-container {
-      display: flex;
-      flex-direction: row-reverse;
-
-      .right-text {
-        font-size: 0.95rem;
-        color: #fff;
-        display: inline;
-        background-color: #267fff;
-        color: #fff;
-        box-shadow: 0 0 0 1px #267fff;
-        border-radius: 10px;
-        padding: 10px;
-        width: auto;
-        overflow-wrap: break-word;
-      }
-    }
-
-    .left-btns,
-    .right-btns {
-      display: flex;
-      flex-direction: row;
-      margin-top: 8px;
-    }
-  }
-
-  // 复制、删除按钮
-  .btn-cus {
-    display: flex;
-    background-color: transparent;
-    align-items: center;
-
-    .btn-image {
-      height: 20px;
-      margin-right: 5px;
-    }
-
-    .btn-cus-text {
-      color: #757575;
-    }
-  }
-
-  .btn-cus:hover {
-    cursor: pointer;
-  }
 
-  .btn-cus:focus {
-    background-color: #8c939d;
+  .message-container {
+    position: absolute;
+    top: 0;
+    bottom: 0;
+    left: 0;
+    right: 0;
+    //width: 100%;
+    //height: 100%;
+    overflow-y: scroll;
+    padding: 0 15px;
   }
 }