消息体渲染优化

This commit is contained in:
WangJing 2025-07-30 15:30:23 +08:00
parent d4277e003a
commit 059b93ddbb
1 changed files with 76 additions and 25 deletions

View File

@ -281,6 +281,8 @@
</div> </div>
</div> </div>
</div> </div>
<div ref="bottomAnchor"></div>
</div> </div>
<!-- 输入区域 --> <!-- 输入区域 -->
@ -382,11 +384,12 @@ import { ref, reactive, nextTick, watch, onMounted } from 'vue'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import VueOfficePdf from '@vue-office/pdf' import VueOfficePdf from '@vue-office/pdf'
import VabChart from '@/plugins/VabChart/index.vue' import VabChart from '@/plugins/VabChart/index.vue'
import { ChevronDownIcon, ChevronUpIcon } from '@heroicons/vue/24/solid'
import { sendChatMessage, type ChatMessageResponse, type ChatMessageSendRequest, stopMessagesStream, getFilePathList, type TraceFile, type TraceData } from '@/api/chat' import { sendChatMessage, type ChatMessageResponse, type ChatMessageSendRequest, stopMessagesStream, getFilePathList, type TraceFile, type TraceData } from '@/api/chat'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { throttle } from 'lodash' import { throttle } from 'lodash'
const expanded = ref(false)
const { t, locale } = useI18n() const { t, locale } = useI18n()
// Props // Props
interface Props { interface Props {
@ -441,11 +444,12 @@ interface Message {
isOpenRemark?: boolean // isOpenRemark?: boolean //
} }
//
const bottomAnchor = ref<HTMLElement | null>(null)
const messages = ref<Message[]>([]) const messages = ref<Message[]>([])
const inputMessage = ref('') const inputMessage = ref('')
const messagesContainer = ref<HTMLElement>() const messagesContainer = ref<HTMLElement | null>(null)
const isLoading = ref(false) const isLoading = ref(false)
const currentTaskId = ref('') const currentTaskId = ref('')
const showStopButton = ref(false) const showStopButton = ref(false)
@ -563,7 +567,7 @@ const createNewChat = async () => {
// //
await nextTick() await nextTick()
await scrollToBottom() throttledScrollToBottom()
} }
// //
@ -632,6 +636,10 @@ watch(messages, (newMessages) => {
} }
}, { deep: true }) }, { deep: true })
//
let isUserScrolling = false
// //
onMounted(async () => { onMounted(async () => {
loadChatHistory() loadChatHistory()
@ -639,6 +647,15 @@ onMounted(async () => {
if (chatHistory.value.length === 0) { if (chatHistory.value.length === 0) {
await createNewChat() await createNewChat()
} }
//
messagesContainer.value?.addEventListener('scroll', () => {
const el = messagesContainer.value
if (!el) return
const threshold = 100
isUserScrolling = el.scrollHeight - el.scrollTop - el.clientHeight > threshold
})
}) })
// ECharts // ECharts
@ -683,10 +700,15 @@ const formatAnswer = async (text: string) => {
// //
const wrappedHtml = html.replace( const wrappedHtml = html.replace(
/<table>/g, /<table>/g,
'<div class="table-container"><table class="excel-table" border="1" width="auto" cellspacing="0" cellpadding="0">' `<details class="table-wrapper" open>
<summary>
Expand / Collapse table
</summary>
<div class="table-container"><table class="excel-table" border="1" width="auto" cellspacing="0" cellpadding="0">`
).replace( ).replace(
/<\/table>/g, /<\/table>/g,
'</table></div>' '</table></div></details>'
).replace( ).replace(
/<th>/g, /<th>/g,
'<th style="border: 1px solid #cbd5e1; padding: 12px; background: #f1f5f9;">' '<th style="border: 1px solid #cbd5e1; padding: 12px; background: #f1f5f9;">'
@ -700,9 +722,9 @@ const formatAnswer = async (text: string) => {
'strong', 'em', 'code', 'pre', 'a', 'br', 'p', 'strong', 'em', 'code', 'pre', 'a', 'br', 'p',
'ul', 'ol', 'li', 'h1', 'h2', 'h3', 'span', 'ul', 'ol', 'li', 'h1', 'h2', 'h3', 'span',
'blockquote', 'hr', 'table', 'thead', 'tbody', 'blockquote', 'hr', 'table', 'thead', 'tbody',
'tr', 'th', 'td', 'div' 'tr', 'th', 'td', 'div', 'details', 'summary'
], ],
ALLOWED_ATTR: ['href', 'target', 'class', 'style', 'data-chart-id', 'border', 'cellspacing', 'cellpadding'] ALLOWED_ATTR: ['href', 'target', 'class', 'style', 'data-chart-id', 'border', 'cellspacing', 'cellpadding', 'open']
}) as string }) as string
console.log('Markdown输入:', processedText) console.log('Markdown输入:', processedText)
@ -801,9 +823,6 @@ const sendMessage = async () => {
currentTaskId.value = data.taskId currentTaskId.value = data.taskId
} }
// messages.value = [...messages.value]
// botMessage.text += data.answer
// await scrollToBottom()
throttledScrollToBottom() throttledScrollToBottom()
} }
} catch (e) { } catch (e) {
@ -824,7 +843,6 @@ const sendMessage = async () => {
const formatted = await formatAnswer(parseThink(botMessage.text).answer) const formatted = await formatAnswer(parseThink(botMessage.text).answer)
botMessage.formattedText = formatted.html botMessage.formattedText = formatted.html
botMessage.echarts = formatted.echarts botMessage.echarts = formatted.echarts
// messages.value = [...messages.value]
if (data.conversationId) { if (data.conversationId) {
newConversationId = data.conversationId newConversationId = data.conversationId
@ -852,7 +870,6 @@ const sendMessage = async () => {
botMessage.sources = data botMessage.sources = data
botMessage.conversationId = newConversationId botMessage.conversationId = newConversationId
botMessage.messageId = newMessageId botMessage.messageId = newMessageId
// messages.value = [...messages.value]
} }
} catch (error) { } catch (error) {
console.log('获取文件来源失败:', error) console.log('获取文件来源失败:', error)
@ -885,20 +902,35 @@ const sendMessage = async () => {
if (messages.value[messages.value.length - 1]?.isLoading) { if (messages.value[messages.value.length - 1]?.isLoading) {
messages.value[messages.value.length - 1].isLoading = false messages.value[messages.value.length - 1].isLoading = false
} }
await scrollToBottom() scrollToBottomForce()
} }
} }
const scrollToBottom = async () => { const scrollToBottomForce = async () => {
await nextTick() await nextTick()
if (messagesContainer.value) { if (messagesContainer.value) {
messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight
} }
} }
//
const scrollToBottom = async () => {
await nextTick()
setTimeout(() => {
requestAnimationFrame(() => {
if (messagesContainer.value && !isUserScrolling) {
bottomAnchor.value.scrollIntoView({
behavior: 'smooth',
block: 'start'
})
}
})
},60)
}
const throttledScrollToBottom = throttle(() => { const throttledScrollToBottom = throttle(() => {
scrollToBottom() scrollToBottom()
}, 150) }, 300,{ leading: true, trailing: true })
// //
const parseThink = (text: string) => { const parseThink = (text: string) => {
@ -1227,7 +1259,7 @@ const regenerateResponse = async (userQuery: string) => {
formattedText: '' formattedText: ''
}) })
messages.value.push(botMessage) messages.value.push(botMessage)
await scrollToBottom() throttledScrollToBottom()
// ID // ID
const currentSession = chatHistory.value[currentChatIndex.value] const currentSession = chatHistory.value[currentChatIndex.value]
@ -1287,8 +1319,7 @@ const regenerateResponse = async (userQuery: string) => {
currentTaskId.value = data.taskId currentTaskId.value = data.taskId
} }
// messages.value = [...messages.value] throttledScrollToBottom()
await scrollToBottom()
} }
} catch (e) { } catch (e) {
console.error('解析错误:', e) console.error('解析错误:', e)
@ -1308,7 +1339,6 @@ const regenerateResponse = async (userQuery: string) => {
const formatted = await formatAnswer(parseThink(botMessage.text).answer) const formatted = await formatAnswer(parseThink(botMessage.text).answer)
botMessage.formattedText = formatted.html botMessage.formattedText = formatted.html
botMessage.echarts = formatted.echarts botMessage.echarts = formatted.echarts
// messages.value = [...messages.value]
if (data.conversationId) { if (data.conversationId) {
newConversationId = data.conversationId newConversationId = data.conversationId
@ -1336,7 +1366,6 @@ const regenerateResponse = async (userQuery: string) => {
botMessage.sources = data botMessage.sources = data
botMessage.conversationId = newConversationId botMessage.conversationId = newConversationId
botMessage.messageId = newMessageId botMessage.messageId = newMessageId
// messages.value = [...messages.value]
} }
} catch (error) { } catch (error) {
console.log('获取文件来源失败:', error) console.log('获取文件来源失败:', error)
@ -1369,7 +1398,7 @@ const regenerateResponse = async (userQuery: string) => {
if (messages.value[messages.value.length - 1]?.isLoading) { if (messages.value[messages.value.length - 1]?.isLoading) {
messages.value[messages.value.length - 1].isLoading = false messages.value[messages.value.length - 1].isLoading = false
} }
await scrollToBottom() scrollToBottomForce()
} }
} }
@ -1399,7 +1428,7 @@ const handleRecommendQuestionClick = (question: string) => {
<style scoped> <style scoped>
.chat-container { .chat-container {
height: 90%; height: 100%;
display: flex; display: flex;
background: #ffffff; background: #ffffff;
position: relative; position: relative;
@ -2130,6 +2159,20 @@ const handleRecommendQuestionClick = (question: string) => {
} }
::v-deep .table-wrapper summary {
cursor: pointer;
display: list-item;
align-items: center;
gap: 8px;
font-weight: 600;
padding: 6px 8px;
background: #f8fafc;
/* border: 1px solid #e2e8f0; */
/* border-radius: 6px; */
font-size: 14px;
}
/* .excel-table th, /* .excel-table th,
.excel-table td { .excel-table td {
white-space: nowrap; white-space: nowrap;
@ -2705,6 +2748,14 @@ const handleRecommendQuestionClick = (question: string) => {
position: relative; position: relative;
} }
::v-deep .icon-chevron {
width: 20px;
height: 20px;
transition: transform 0.3s ease;
color: #333; /* 让箭头颜色和文字颜色一致 */
}
.chart-wrapper { .chart-wrapper {
margin-bottom: 1.5rem; margin-bottom: 1.5rem;
background: #ffffff; background: #ffffff;