From a1d7e81859f554f3a53680cc35f0f49bf1f77098 Mon Sep 17 00:00:00 2001
From: wwf <1971391498@qq.com>
Date: 星期四, 14 五月 2026 14:37:02 +0800
Subject: [PATCH] 导入项目

---
 src/views/ai/chat/index/index.vue |  630 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 files changed, 630 insertions(+), 0 deletions(-)

diff --git a/src/views/ai/chat/index/index.vue b/src/views/ai/chat/index/index.vue
new file mode 100644
index 0000000..8a4deee
--- /dev/null
+++ b/src/views/ai/chat/index/index.vue
@@ -0,0 +1,630 @@
+<template>
+  <el-container class="absolute flex-1 top-0 left-0 h-full w-full">
+    <!-- 宸︿晶锛氬璇濆垪琛� -->
+    <ConversationList
+      :active-id="activeConversationId?.toString() || ''"
+      ref="conversationListRef"
+      @on-conversation-create="handleConversationCreateSuccess"
+      @on-conversation-click="handleConversationClick"
+      @on-conversation-clear="handleConversationClear"
+      @on-conversation-delete="handlerConversationDelete"
+    />
+    <!-- 鍙充晶锛氬璇濊鎯� -->
+    <el-container class="bg-[var(--el-bg-color)]">
+      <el-header
+        class="flex flex-row items-center justify-between bg-[var(--el-bg-color-page)] shadow-[0_0_0_0_var(--el-border-color-light)]"
+      >
+        <div class="text-18px font-bold">
+          {{ activeConversation?.title ? activeConversation?.title : '瀵硅瘽' }}
+          <span v-if="activeMessageList.length">({{ activeMessageList.length }})</span>
+        </div>
+        <div class="flex w-300px flex-row justify-end" v-if="activeConversation">
+          <el-button type="primary" bg plain size="small" @click="openChatConversationUpdateForm">
+            <span v-html="activeConversation?.modelName"></span>
+            <Icon icon="ep:setting" class="ml-10px" />
+          </el-button>
+          <el-button size="small" class="p-10px" @click="handlerMessageClear">
+            <Icon
+              icon="heroicons-outline:archive-box-x-mark"
+              color="var(--el-text-color-placeholder)"
+            />
+          </el-button>
+          <el-button size="small" class="p-10px">
+            <Icon icon="ep:download" color="var(--el-text-color-placeholder)" />
+          </el-button>
+          <el-button size="small" class="p-10px" @click="handleGoTopMessage">
+            <Icon icon="ep:top" color="var(--el-text-color-placeholder)" />
+          </el-button>
+        </div>
+      </el-header>
+
+      <!-- main锛氭秷鎭垪琛� -->
+      <el-main class="m-0 p-0 relative h-full w-full">
+        <div>
+          <div class="absolute top-0 bottom-0 left-0 right-0 overflow-y-hidden p-0 m-0">
+            <!-- 鎯呭喌涓�锛氭秷鎭姞杞戒腑 -->
+            <MessageLoading v-if="activeMessageListLoading" />
+            <!-- 鎯呭喌浜岋細鏃犺亰澶╁璇濇椂 -->
+            <MessageNewConversation
+              v-if="!activeConversation"
+              @on-new-conversation="handleConversationCreate"
+            />
+            <!-- 鎯呭喌涓夛細娑堟伅鍒楄〃涓虹┖ -->
+            <MessageListEmpty
+              v-if="!activeMessageListLoading && messageList.length === 0 && activeConversation"
+              @on-prompt="doSendMessage"
+            />
+            <!-- 鎯呭喌鍥涳細娑堟伅鍒楄〃涓嶄负绌� -->
+            <MessageList
+              v-if="!activeMessageListLoading && messageList.length > 0 && activeConversation"
+              ref="messageRef"
+              :conversation="activeConversation"
+              :list="messageList"
+              @on-delete-success="handleMessageDelete"
+              @on-edit="handleMessageEdit"
+              @on-refresh="handleMessageRefresh"
+            />
+          </div>
+        </div>
+      </el-main>
+
+      <!-- 搴曢儴 -->
+      <el-footer class="flex flex-col !h-auto !p-0">
+        <!-- TODO @鑺嬭壙锛氳繖鍧楄鎯冲姙娉曡縼绉讳笅锛� -->
+        <form
+          class="mt-10px mx-20px mb-20px py-9px px-10px flex flex-col h-auto rounded-10px"
+          style="border: 1px solid var(--el-border-color)"
+        >
+          <textarea
+            class="h-80px border-none box-border resize-none py-0 px-2px overflow-auto focus:outline-none"
+            v-model="prompt"
+            @keydown="handleSendByKeydown"
+            @input="handlePromptInput"
+            @compositionstart="onCompositionstart"
+            @compositionend="onCompositionend"
+            placeholder="闂垜浠讳綍闂...锛圫hift+Enter 鎹㈣锛屾寜涓� Enter 鍙戦�侊級"
+          >
+          </textarea>
+          <div class="flex justify-between pb-0 pt-5px">
+            <div class="flex items-center">
+              <MessageFileUpload v-model="uploadFiles" :limit="5" :max-size="10" class="mr-10px" />
+              <el-switch v-model="enableContext" />
+              <span class="ml-5px mr-15px text-14px text-#8f8f8f">涓婁笅鏂�</span>
+              <el-switch v-model="enableWebSearch" />
+              <span class="ml-5px text-14px text-#8f8f8f">鑱旂綉鎼滅储</span>
+            </div>
+            <el-button
+              type="primary"
+              size="default"
+              @click="handleSendByButton"
+              :loading="conversationInProgress"
+              v-if="conversationInProgress == false"
+            >
+              {{ conversationInProgress ? '杩涜涓�' : '鍙戦��' }}
+            </el-button>
+            <el-button
+              type="danger"
+              size="default"
+              @click="stopStream()"
+              v-if="conversationInProgress == true"
+            >
+              鍋滄
+            </el-button>
+          </div>
+        </form>
+      </el-footer>
+    </el-container>
+
+    <!-- 鏇存柊瀵硅瘽 Form -->
+    <ConversationUpdateForm
+      ref="conversationUpdateFormRef"
+      @success="handleConversationUpdateSuccess"
+    />
+  </el-container>
+</template>
+
+<script setup lang="ts">
+import { ChatMessageApi, ChatMessageVO } from '@/api/ai/chat/message'
+import { ChatConversationApi, ChatConversationVO } from '@/api/ai/chat/conversation'
+import ConversationList from './components/conversation/ConversationList.vue'
+import ConversationUpdateForm from './components/conversation/ConversationUpdateForm.vue'
+import MessageList from './components/message/MessageList.vue'
+import MessageListEmpty from './components/message/MessageListEmpty.vue'
+import MessageLoading from './components/message/MessageLoading.vue'
+import MessageNewConversation from './components/message/MessageNewConversation.vue'
+import MessageFileUpload from './components/message/MessageFileUpload.vue'
+
+/** AI 鑱婂ぉ瀵硅瘽 鍒楄〃 */
+defineOptions({ name: 'AiChat' })
+
+const route = useRoute() // 璺敱
+const message = useMessage() // 娑堟伅寮圭獥
+
+// 鑱婂ぉ瀵硅瘽
+const conversationListRef = ref()
+const activeConversationId = ref<number | null>(null) // 閫変腑鐨勫璇濈紪鍙�
+const activeConversation = ref<ChatConversationVO | null>(null) // 閫変腑鐨� Conversation
+const conversationInProgress = ref(false) // 瀵硅瘽鏄惁姝e湪杩涜涓�傜洰鍓嶅彧鏈夈�愬彂閫併�戞秷鎭椂锛屼細鏇存柊涓� true锛岄伩鍏嶅垏鎹㈠璇濄�佸垹闄ゅ璇濈瓑鎿嶄綔
+
+// 娑堟伅鍒楄〃
+const messageRef = ref()
+const activeMessageList = ref<ChatMessageVO[]>([]) // 閫変腑瀵硅瘽鐨勬秷鎭垪琛�
+const activeMessageListLoading = ref<boolean>(false) // activeMessageList 鏄惁姝e湪鍔犺浇涓�
+const activeMessageListLoadingTimer = ref<any>() // activeMessageListLoading Timer 瀹氭椂鍣ㄣ�傚鏋滃姞杞介�熷害寰堝揩锛屽氨涓嶈繘鍏ュ姞杞戒腑
+// 娑堟伅婊氬姩
+const textSpeed = ref<number>(50) // Typing speed in milliseconds
+const textRoleRunning = ref<boolean>(false) // Typing speed in milliseconds
+
+// 鍙戦�佹秷鎭緭鍏ユ
+const isComposing = ref(false) // 鍒ゆ柇鐢ㄦ埛鏄惁鍦ㄨ緭鍏�
+const conversationInAbortController = ref<any>() // 瀵硅瘽杩涜涓� abort 鎺у埗鍣�(鎺у埗 stream 瀵硅瘽)
+const inputTimeout = ref<any>() // 澶勭悊杈撳叆涓洖杞︾殑瀹氭椂鍣�
+const prompt = ref<string>() // prompt
+const enableContext = ref<boolean>(true) // 鏄惁寮�鍚笂涓嬫枃
+const enableWebSearch = ref<boolean>(false) // 鏄惁寮�鍚仈缃戞悳绱�
+const uploadFiles = ref<string[]>([]) // 涓婁紶鐨勬枃浠� URL 鍒楄〃
+// 鎺ユ敹 Stream 娑堟伅
+const receiveMessageFullText = ref('')
+const receiveMessageDisplayedText = ref('')
+
+// =========== 銆愯亰澶╁璇濄�戠浉鍏� ===========
+
+/** 鑾峰彇瀵硅瘽淇℃伅 */
+const getConversation = async (id: number | null) => {
+  if (!id) {
+    return
+  }
+  const conversation: ChatConversationVO = await ChatConversationApi.getChatConversationMy(id)
+  if (!conversation) {
+    return
+  }
+  activeConversation.value = conversation
+  activeConversationId.value = conversation.id
+}
+
+/**
+ * 鐐瑰嚮鏌愪釜瀵硅瘽
+ *
+ * @param conversation 閫変腑鐨勫璇�
+ * @return 鏄惁鍒囨崲鎴愬姛
+ */
+const handleConversationClick = async (conversation: ChatConversationVO) => {
+  // 瀵硅瘽杩涜涓紝涓嶅厑璁稿垏鎹�
+  if (conversationInProgress.value) {
+    message.alert('瀵硅瘽涓紝涓嶅厑璁稿垏鎹�!')
+    return false
+  }
+
+  // 鏇存柊閫変腑鐨勫璇� id
+  activeConversationId.value = conversation.id
+  activeConversation.value = conversation
+  // 鍒锋柊 message 鍒楄〃
+  await getMessageList()
+  // 婊氬姩搴曢儴
+  scrollToBottom(true)
+  // 娓呯┖杈撳叆妗�
+  prompt.value = ''
+  // 娓呯┖鏂囦欢鍒楄〃
+  uploadFiles.value = []
+  return true
+}
+
+/** 鍒犻櫎鏌愪釜瀵硅瘽*/
+const handlerConversationDelete = async (delConversation: ChatConversationVO) => {
+  // 鍒犻櫎鐨勫璇濆鏋滄槸褰撳墠閫変腑鐨勶紝閭d箞灏遍噸缃�
+  if (activeConversationId.value === delConversation.id) {
+    await handleConversationClear()
+  }
+}
+/** 娓呯┖閫変腑鐨勫璇� */
+const handleConversationClear = async () => {
+  // 瀵硅瘽杩涜涓紝涓嶅厑璁稿垏鎹�
+  if (conversationInProgress.value) {
+    message.alert('瀵硅瘽涓紝涓嶅厑璁稿垏鎹�!')
+    return false
+  }
+  activeConversationId.value = null
+  activeConversation.value = null
+  activeMessageList.value = []
+}
+
+/** 淇敼鑱婂ぉ瀵硅瘽 */
+const conversationUpdateFormRef = ref()
+const openChatConversationUpdateForm = async () => {
+  conversationUpdateFormRef.value.open(activeConversationId.value)
+}
+const handleConversationUpdateSuccess = async () => {
+  // 瀵硅瘽鏇存柊鎴愬姛锛屽埛鏂版渶鏂颁俊鎭�
+  await getConversation(activeConversationId.value)
+}
+
+/** 澶勭悊鑱婂ぉ瀵硅瘽鐨勫垱寤烘垚鍔� */
+const handleConversationCreate = async () => {
+  // 鍒涘缓瀵硅瘽
+  await conversationListRef.value.createConversation()
+}
+/** 澶勭悊鑱婂ぉ瀵硅瘽鐨勫垱寤烘垚鍔� */
+const handleConversationCreateSuccess = async () => {
+  // 鍒涘缓鏂扮殑瀵硅瘽锛屾竻绌鸿緭鍏ユ
+  prompt.value = ''
+  // 娓呯┖鏂囦欢鍒楄〃
+  uploadFiles.value = []
+}
+
+// =========== 銆愭秷鎭垪琛ㄣ�戠浉鍏� ===========
+
+/** 鑾峰彇娑堟伅 message 鍒楄〃 */
+const getMessageList = async () => {
+  try {
+    if (activeConversationId.value === null) {
+      return
+    }
+    // Timer 瀹氭椂鍣紝濡傛灉鍔犺浇閫熷害寰堝揩锛屽氨涓嶈繘鍏ュ姞杞戒腑
+    activeMessageListLoadingTimer.value = setTimeout(() => {
+      activeMessageListLoading.value = true
+    }, 60)
+
+    // 鑾峰彇娑堟伅鍒楄〃
+    activeMessageList.value = await ChatMessageApi.getChatMessageListByConversationId(
+      activeConversationId.value
+    )
+
+    // 婊氬姩鍒版渶涓嬮潰
+    await nextTick()
+    await scrollToBottom()
+  } finally {
+    // time 瀹氭椂鍣紝濡傛灉鍔犺浇閫熷害寰堝揩锛屽氨涓嶈繘鍏ュ姞杞戒腑
+    if (activeMessageListLoadingTimer.value) {
+      clearTimeout(activeMessageListLoadingTimer.value)
+    }
+    // 鍔犺浇缁撴潫
+    activeMessageListLoading.value = false
+  }
+}
+
+/**
+ * 娑堟伅鍒楄〃
+ *
+ * 鍜� {@link #getMessageList()} 鐨勫樊寮傛槸锛屾妸 systemMessage 鑰冭檻杩涘幓
+ */
+const messageList = computed(() => {
+  if (activeMessageList.value.length > 0) {
+    return activeMessageList.value
+  }
+  // 娌℃湁娑堟伅鏃讹紝濡傛灉鏈� systemMessage 鍒欏睍绀哄畠
+  if (activeConversation.value?.systemMessage) {
+    return [
+      {
+        id: 0,
+        conversationId: activeConversation.value.id || 0,
+        type: 'system',
+        userId: '',
+        roleId: '',
+        model: 0,
+        modelId: 0,
+        content: activeConversation.value.systemMessage,
+        tokens: 0,
+        createTime: new Date(),
+        roleAvatar: '',
+        userAvatar: ''
+      } as ChatMessageVO
+    ]
+  }
+  return []
+})
+
+/** 澶勭悊鍒犻櫎 message 娑堟伅 */
+const handleMessageDelete = () => {
+  if (conversationInProgress.value) {
+    message.alert('鍥炵瓟涓紝涓嶈兘鍒犻櫎!')
+    return
+  }
+  // 鍒锋柊 message 鍒楄〃
+  getMessageList()
+}
+
+/** 澶勭悊 message 娓呯┖ */
+const handlerMessageClear = async () => {
+  if (!activeConversationId.value) {
+    return
+  }
+  try {
+    // 纭鎻愮ず
+    await message.delConfirm('纭娓呯┖瀵硅瘽娑堟伅锛�')
+    // 娓呯┖瀵硅瘽
+    await ChatMessageApi.deleteByConversationId(activeConversationId.value)
+    // 鍒锋柊 message 鍒楄〃
+    activeMessageList.value = []
+  } catch {}
+}
+
+/** 鍥炲埌 message 鍒楄〃鐨勯《閮� */
+const handleGoTopMessage = () => {
+  messageRef.value.handlerGoTop()
+}
+
+// =========== 銆愬彂閫佹秷鎭�戠浉鍏� ===========
+
+/** 澶勭悊鏉ヨ嚜 keydown 鐨勫彂閫佹秷鎭� */
+const handleSendByKeydown = async (event) => {
+  // 鍒ゆ柇鐢ㄦ埛鏄惁鍦ㄨ緭鍏�
+  if (isComposing.value) {
+    return
+  }
+  // 杩涜涓笉鍏佽鍙戦��
+  if (conversationInProgress.value) {
+    return
+  }
+  const content = prompt.value?.trim() as string
+  if (event.key === 'Enter') {
+    if (event.shiftKey) {
+      // 鎻掑叆鎹㈣
+      prompt.value += '\r\n'
+      event.preventDefault() // 闃叉榛樿鐨勬崲琛岃涓�
+    } else {
+      // 鍙戦�佹秷鎭�
+      await doSendMessage(content)
+      event.preventDefault() // 闃叉榛樿鐨勬彁浜よ涓�
+    }
+  }
+}
+
+/** 澶勭悊鏉ヨ嚜銆愬彂閫併�戞寜閽殑鍙戦�佹秷鎭� */
+const handleSendByButton = () => {
+  doSendMessage(prompt.value?.trim() as string)
+}
+
+/** 澶勭悊 prompt 杈撳叆鍙樺寲 */
+const handlePromptInput = (event) => {
+  // 闈炶緭鍏ユ硶 杈撳叆璁剧疆涓� true
+  if (!isComposing.value) {
+    // 鍥炶溅 event data 鏄� null
+    if (event.data == null) {
+      return
+    }
+    isComposing.value = true
+  }
+  // 娓呯悊瀹氭椂鍣�
+  if (inputTimeout.value) {
+    clearTimeout(inputTimeout.value)
+  }
+  // 閲嶇疆瀹氭椂鍣�
+  inputTimeout.value = setTimeout(() => {
+    isComposing.value = false
+  }, 400)
+}
+// TODO @鑺嬭壙锛氭槸涓嶆槸鍙互閫氳繃 @keydown.enter銆丂keydown.shift.enter 鏉ュ疄鐜帮紝鍥炶溅鍙戦�併�乻hift+鍥炶溅鎹㈣锛涗富瑕佺湅鐪嬶紝鏄笉鏄彲浠ョ畝鍖� isComposing 鐩稿叧鐨勯�昏緫
+const onCompositionstart = () => {
+  isComposing.value = true
+}
+const onCompositionend = () => {
+  // console.log('杈撳叆缁撴潫...')
+  setTimeout(() => {
+    isComposing.value = false
+  }, 200)
+}
+
+/** 鐪熸鎵ц銆愬彂閫併�戞秷鎭搷浣� */
+const doSendMessage = async (content: string) => {
+  // 鏍¢獙
+  if (content.length < 1) {
+    message.error('鍙戦�佸け璐ワ紝鍘熷洜锛氬唴瀹逛负绌猴紒')
+    return
+  }
+  if (activeConversationId.value == null) {
+    message.error('杩樻病鍒涘缓瀵硅瘽锛屼笉鑳藉彂閫�!')
+    return
+  }
+
+  // 鍑嗗闄勪欢 URL 鏁扮粍
+  const attachmentUrls = [...uploadFiles.value]
+
+  // 娓呯┖杈撳叆妗嗗拰鏂囦欢鍒楄〃
+  prompt.value = ''
+  uploadFiles.value = []
+
+  // 鎵ц鍙戦��
+  await doSendMessageStream({
+    conversationId: activeConversationId.value,
+    content: content,
+    attachmentUrls: attachmentUrls
+  } as ChatMessageVO)
+}
+
+/** 鐪熸鎵ц銆愬彂閫併�戞秷鎭搷浣� */
+const doSendMessageStream = async (userMessage: ChatMessageVO) => {
+  // 鍒涘缓 AbortController 瀹炰緥锛屼互渚夸腑姝㈣姹�
+  conversationInAbortController.value = new AbortController()
+  // 鏍囪瀵硅瘽杩涜涓�
+  conversationInProgress.value = true
+  // 璁剧疆涓虹┖
+  receiveMessageFullText.value = ''
+
+  try {
+    // 1.1 鍏堟坊鍔犱袱涓亣鏁版嵁锛岀瓑 stream 杩斿洖鍐嶆浛鎹�
+    activeMessageList.value.push({
+      id: -1,
+      conversationId: activeConversationId.value,
+      type: 'user',
+      content: userMessage.content,
+      attachmentUrls: userMessage.attachmentUrls || [],
+      createTime: new Date()
+    } as ChatMessageVO)
+    activeMessageList.value.push({
+      id: -2,
+      conversationId: activeConversationId.value,
+      type: 'assistant',
+      content: '鎬濊�冧腑...',
+      reasoningContent: '',
+      createTime: new Date()
+    } as ChatMessageVO)
+    // 1.2 婊氬姩鍒版渶涓嬮潰
+    await nextTick()
+    await scrollToBottom() // 搴曢儴
+    // 1.3 寮�濮嬫粴鍔�
+    textRoll()
+
+    // 2. 鍙戦�� event stream
+    let isFirstChunk = true // 鏄惁鏄涓�涓� chunk 娑堟伅娈�
+    await ChatMessageApi.sendChatMessageStream(
+      userMessage.conversationId,
+      userMessage.content,
+      conversationInAbortController.value,
+      enableContext.value,
+      enableWebSearch.value,
+      async (res) => {
+        const { code, data, msg } = JSON.parse(res.data)
+        if (code !== 0) {
+          message.alert(`瀵硅瘽寮傚父! ${msg}`)
+          // 濡傛灉鏈帴鏀跺埌娑堟伅锛屽垯杩涜鍒犻櫎
+          if (receiveMessageFullText.value === '') {
+            activeMessageList.value.pop()
+          }
+          return
+        }
+
+        // 濡傛灉鍐呭涓虹┖锛屽氨涓嶅鐞嗐��
+        if (data.receive.content === '' && !data.receive.reasoningContent) {
+          return
+        }
+
+        // 棣栨杩斿洖闇�瑕佹坊鍔犱竴涓� message 鍒伴〉闈紝鍚庨潰鐨勯兘鏄洿鏂�
+        if (isFirstChunk) {
+          isFirstChunk = false
+          // 寮瑰嚭涓や釜鍋囨暟鎹�
+          activeMessageList.value.pop()
+          activeMessageList.value.pop()
+          // 鏇存柊杩斿洖鐨勬暟鎹�
+          activeMessageList.value.push(data.send)
+          data.send.attachmentUrls = userMessage.attachmentUrls
+          activeMessageList.value.push(data.receive)
+        }
+
+        // 澶勭悊 reasoningContent
+        if (data.receive.reasoningContent) {
+          const lastMessage = activeMessageList.value[activeMessageList.value.length - 1]
+          lastMessage.reasoningContent =
+            lastMessage.reasoningContent + data.receive.reasoningContent
+        }
+
+        // 澶勭悊姝e父鍐呭
+        if (data.receive.content !== '') {
+          receiveMessageFullText.value = receiveMessageFullText.value + data.receive.content
+        }
+        // 婊氬姩鍒版渶涓嬮潰
+        await scrollToBottom()
+      },
+      (error: any) => {
+        // 寮傚父鎻愮ず锛屽苟鍋滄娴�
+        message.alert(`瀵硅瘽寮傚父锛乣)
+        stopStream()
+        // 闇�瑕佹姏鍑哄紓甯革紝绂佹閲嶈瘯
+        throw error
+      },
+      () => {
+        stopStream()
+      },
+      userMessage.attachmentUrls
+    )
+  } catch {}
+}
+
+/** 鍋滄 stream 娴佸紡璋冪敤 */
+const stopStream = async () => {
+  // tip锛氬鏋� stream 杩涜涓殑 message锛屽氨闇�瑕佽皟鐢� controller 缁撴潫
+  if (conversationInAbortController.value) {
+    conversationInAbortController.value.abort()
+  }
+  // 璁剧疆涓� false
+  conversationInProgress.value = false
+}
+
+/** 缂栬緫 message锛氳缃负 prompt锛屽彲浠ュ啀娆$紪杈� */
+const handleMessageEdit = (message: ChatMessageVO) => {
+  prompt.value = message.content
+}
+
+/** 鍒锋柊 message锛氬熀浜庢寚瀹氭秷鎭紝鍐嶆鍙戣捣瀵硅瘽 */
+const handleMessageRefresh = (message: ChatMessageVO) => {
+  doSendMessage(message.content)
+}
+
+// ============== 銆愭秷鎭粴鍔ㄣ�戠浉鍏� =============
+
+/** 婊氬姩鍒� message 搴曢儴 */
+const scrollToBottom = async (isIgnore?: boolean) => {
+  await nextTick()
+  if (messageRef.value) {
+    messageRef.value.scrollToBottom(isIgnore)
+  }
+}
+
+/** 鑷彁婊氬姩鏁堟灉 */
+const textRoll = async () => {
+  let index = 0
+  try {
+    // 鍙兘鎵ц涓�娆�
+    if (textRoleRunning.value) {
+      return
+    }
+    // 璁剧疆鐘舵��
+    textRoleRunning.value = true
+    receiveMessageDisplayedText.value = ''
+    const task = async () => {
+      // 璋冩暣閫熷害
+      const diff =
+        (receiveMessageFullText.value.length - receiveMessageDisplayedText.value.length) / 10
+      if (diff > 5) {
+        textSpeed.value = 10
+      } else if (diff > 2) {
+        textSpeed.value = 30
+      } else if (diff > 1.5) {
+        textSpeed.value = 50
+      } else {
+        textSpeed.value = 100
+      }
+      // 瀵硅瘽缁撴潫锛屽氨鎸� 30 鐨勯�熷害
+      if (!conversationInProgress.value) {
+        textSpeed.value = 10
+      }
+
+      if (index < receiveMessageFullText.value.length) {
+        receiveMessageDisplayedText.value += receiveMessageFullText.value[index]
+        index++
+
+        // 鏇存柊 message
+        const lastMessage = activeMessageList.value[activeMessageList.value.length - 1]
+        lastMessage.content = receiveMessageDisplayedText.value
+        // 婊氬姩鍒颁綇涓嬮潰
+        await scrollToBottom()
+        // 閲嶆柊璁剧疆浠诲姟
+        timer = setTimeout(task, textSpeed.value)
+      } else {
+        // 涓嶆槸瀵硅瘽涓彲浠ョ粨鏉�
+        if (!conversationInProgress.value) {
+          textRoleRunning.value = false
+          clearTimeout(timer)
+        } else {
+          // 閲嶆柊璁剧疆浠诲姟
+          timer = setTimeout(task, textSpeed.value)
+        }
+      }
+    }
+    let timer = setTimeout(task, textSpeed.value)
+  } catch {}
+}
+
+/** 鍒濆鍖� **/
+onMounted(async () => {
+  // 濡傛灉鏈� conversationId 鍙傛暟锛屽垯榛樿閫変腑
+  if (route.query.conversationId) {
+    const id = route.query.conversationId as unknown as number
+    activeConversationId.value = id
+    await getConversation(id)
+  }
+
+  // 鑾峰彇鍒楄〃鏁版嵁
+  activeMessageListLoading.value = true
+  await getMessageList()
+})
+</script>

--
Gitblit v1.8.0