消息体渲染优化
This commit is contained in:
parent
d4277e003a
commit
059b93ddbb
|
@ -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;
|
||||
|
|
Loading…
Reference in New Issue