消息体渲染优化

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 ref="bottomAnchor"></div>
</div>
<!-- 输入区域 -->
@ -382,11 +384,12 @@ import { ref, reactive, nextTick, watch, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
import VueOfficePdf from '@vue-office/pdf'
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 { useI18n } from 'vue-i18n'
import { throttle } from 'lodash'
const expanded = ref(false)
const { t, locale } = useI18n()
// Props
interface Props {
@ -441,11 +444,12 @@ interface Message {
isOpenRemark?: boolean //
}
//
const bottomAnchor = ref<HTMLElement | null>(null)
const messages = ref<Message[]>([])
const inputMessage = ref('')
const messagesContainer = ref<HTMLElement>()
const messagesContainer = ref<HTMLElement | null>(null)
const isLoading = ref(false)
const currentTaskId = ref('')
const showStopButton = ref(false)
@ -563,7 +567,7 @@ const createNewChat = async () => {
//
await nextTick()
await scrollToBottom()
throttledScrollToBottom()
}
//
@ -632,6 +636,10 @@ watch(messages, (newMessages) => {
}
}, { deep: true })
//
let isUserScrolling = false
//
onMounted(async () => {
loadChatHistory()
@ -639,6 +647,15 @@ onMounted(async () => {
if (chatHistory.value.length === 0) {
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
@ -683,10 +700,15 @@ const formatAnswer = async (text: string) => {
//
const wrappedHtml = html.replace(
/<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(
/<\/table>/g,
'</table></div>'
/<\/table>/g,
'</table></div></details>'
).replace(
/<th>/g,
'<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',
'ul', 'ol', 'li', 'h1', 'h2', 'h3', 'span',
'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
console.log('Markdown输入:', processedText)
@ -801,9 +823,6 @@ const sendMessage = async () => {
currentTaskId.value = data.taskId
}
// messages.value = [...messages.value]
// botMessage.text += data.answer
// await scrollToBottom()
throttledScrollToBottom()
}
} catch (e) {
@ -824,7 +843,6 @@ const sendMessage = async () => {
const formatted = await formatAnswer(parseThink(botMessage.text).answer)
botMessage.formattedText = formatted.html
botMessage.echarts = formatted.echarts
// messages.value = [...messages.value]
if (data.conversationId) {
newConversationId = data.conversationId
@ -852,7 +870,6 @@ const sendMessage = async () => {
botMessage.sources = data
botMessage.conversationId = newConversationId
botMessage.messageId = newMessageId
// messages.value = [...messages.value]
}
} catch (error) {
console.log('获取文件来源失败:', error)
@ -885,20 +902,35 @@ const sendMessage = async () => {
if (messages.value[messages.value.length - 1]?.isLoading) {
messages.value[messages.value.length - 1].isLoading = false
}
await scrollToBottom()
scrollToBottomForce()
}
}
const scrollToBottom = async () => {
const scrollToBottomForce = async () => {
await nextTick()
if (messagesContainer.value) {
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(() => {
scrollToBottom()
}, 150)
scrollToBottom()
}, 300,{ leading: true, trailing: true })
//
const parseThink = (text: string) => {
@ -1227,7 +1259,7 @@ const regenerateResponse = async (userQuery: string) => {
formattedText: ''
})
messages.value.push(botMessage)
await scrollToBottom()
throttledScrollToBottom()
// ID
const currentSession = chatHistory.value[currentChatIndex.value]
@ -1287,8 +1319,7 @@ const regenerateResponse = async (userQuery: string) => {
currentTaskId.value = data.taskId
}
// messages.value = [...messages.value]
await scrollToBottom()
throttledScrollToBottom()
}
} catch (e) {
console.error('解析错误:', e)
@ -1308,7 +1339,6 @@ const regenerateResponse = async (userQuery: string) => {
const formatted = await formatAnswer(parseThink(botMessage.text).answer)
botMessage.formattedText = formatted.html
botMessage.echarts = formatted.echarts
// messages.value = [...messages.value]
if (data.conversationId) {
newConversationId = data.conversationId
@ -1336,7 +1366,6 @@ const regenerateResponse = async (userQuery: string) => {
botMessage.sources = data
botMessage.conversationId = newConversationId
botMessage.messageId = newMessageId
// messages.value = [...messages.value]
}
} catch (error) {
console.log('获取文件来源失败:', error)
@ -1369,7 +1398,7 @@ const regenerateResponse = async (userQuery: string) => {
if (messages.value[messages.value.length - 1]?.isLoading) {
messages.value[messages.value.length - 1].isLoading = false
}
await scrollToBottom()
scrollToBottomForce()
}
}
@ -1399,7 +1428,7 @@ const handleRecommendQuestionClick = (question: string) => {
<style scoped>
.chat-container {
height: 90%;
height: 100%;
display: flex;
background: #ffffff;
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 td {
white-space: nowrap;
@ -2705,6 +2748,14 @@ const handleRecommendQuestionClick = (question: string) => {
position: relative;
}
::v-deep .icon-chevron {
width: 20px;
height: 20px;
transition: transform 0.3s ease;
color: #333; /* 让箭头颜色和文字颜色一致 */
}
.chart-wrapper {
margin-bottom: 1.5rem;
background: #ffffff;