2025-11-10 23:53:05 +08:00
|
|
|
|
<template>
|
|
|
|
|
|
<div class="prompt-display" v-html="renderedContent"></div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script setup>
|
2025-11-23 01:40:59 +08:00
|
|
|
|
import { ref, watch, nextTick, onUnmounted, onMounted } from 'vue'
|
2025-11-10 23:53:05 +08:00
|
|
|
|
import { renderMarkdown } from '@/utils/markdown'
|
|
|
|
|
|
|
|
|
|
|
|
const props = defineProps({
|
|
|
|
|
|
content: {
|
|
|
|
|
|
type: String,
|
|
|
|
|
|
default: ''
|
|
|
|
|
|
},
|
|
|
|
|
|
isStreaming: {
|
|
|
|
|
|
type: Boolean,
|
|
|
|
|
|
default: false
|
|
|
|
|
|
}
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2025-11-22 18:30:02 +08:00
|
|
|
|
// 内部维护的纯文本内容(用于计算增量和渲染源)
|
|
|
|
|
|
const internalContent = ref('')
|
|
|
|
|
|
// 最终渲染的 HTML
|
2025-11-13 01:06:28 +08:00
|
|
|
|
const renderedContent = ref('')
|
2025-11-10 23:53:05 +08:00
|
|
|
|
|
2025-11-22 18:30:02 +08:00
|
|
|
|
let debounceTimer = null
|
2025-11-23 01:40:59 +08:00
|
|
|
|
// 页面可见性状态
|
|
|
|
|
|
const isPageVisible = ref(true)
|
2025-11-19 00:12:47 +08:00
|
|
|
|
|
2025-11-22 18:30:02 +08:00
|
|
|
|
// 仅在流式时计算并追加增量
|
|
|
|
|
|
function appendStreamingDelta(newFullContent) {
|
2025-11-23 01:40:59 +08:00
|
|
|
|
// 页面不可见时,跳过更新避免重复片段
|
|
|
|
|
|
if (!isPageVisible.value) {
|
|
|
|
|
|
console.log('[ChatMessageRenderer] 页面不可见,跳过流式更新')
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-22 18:30:02 +08:00
|
|
|
|
const prev = internalContent.value
|
|
|
|
|
|
if (newFullContent.startsWith(prev)) {
|
|
|
|
|
|
// 正常情况:新内容包含旧内容 → 只追加差值
|
|
|
|
|
|
const delta = newFullContent.slice(prev.length)
|
|
|
|
|
|
internalContent.value += delta
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 异常情况(如后端重发了不连续的内容),直接覆盖防止乱序
|
2025-11-23 01:40:59 +08:00
|
|
|
|
console.warn('[ChatMessageRenderer] Streaming content out of order, forcing replace')
|
2025-11-22 18:30:02 +08:00
|
|
|
|
internalContent.value = newFullContent
|
2025-11-19 00:12:47 +08:00
|
|
|
|
}
|
2025-11-22 18:30:02 +08:00
|
|
|
|
}
|
2025-11-19 00:12:47 +08:00
|
|
|
|
|
2025-11-22 18:30:02 +08:00
|
|
|
|
// 更新 Markdown 渲染
|
|
|
|
|
|
async function updateRendered() {
|
|
|
|
|
|
if (!internalContent.value) {
|
2025-11-13 01:06:28 +08:00
|
|
|
|
renderedContent.value = ''
|
2025-11-10 23:53:05 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
2025-11-22 18:30:02 +08:00
|
|
|
|
// renderMarkdown 可能包含异步操作(如 highlight.js),用 nextTick 确保 DOM 就绪
|
|
|
|
|
|
renderedContent.value = await renderMarkdown(internalContent.value)
|
2025-11-10 23:53:05 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-22 18:30:02 +08:00
|
|
|
|
// 主更新逻辑
|
|
|
|
|
|
function updateContent(newContent = '') {
|
|
|
|
|
|
if (props.isStreaming) {
|
|
|
|
|
|
appendStreamingDelta(newContent)
|
|
|
|
|
|
} else {
|
|
|
|
|
|
internalContent.value = newContent
|
2025-11-10 23:53:05 +08:00
|
|
|
|
}
|
2025-11-19 00:12:47 +08:00
|
|
|
|
|
2025-11-22 18:30:02 +08:00
|
|
|
|
// 流式直接渲染,非流式防抖(避免频繁完整替换导致光标跳动或闪烁)
|
2025-11-22 17:17:15 +08:00
|
|
|
|
if (props.isStreaming) {
|
2025-11-22 18:30:02 +08:00
|
|
|
|
updateRendered()
|
2025-11-22 17:17:15 +08:00
|
|
|
|
} else {
|
2025-11-22 18:30:02 +08:00
|
|
|
|
clearTimeout(debounceTimer)
|
|
|
|
|
|
debounceTimer = setTimeout(() => {
|
|
|
|
|
|
updateRendered()
|
|
|
|
|
|
}, 60) // 60ms 足够平滑且不卡顿
|
2025-11-22 17:17:15 +08:00
|
|
|
|
}
|
2025-11-10 23:53:05 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-22 18:30:02 +08:00
|
|
|
|
// 监听 content 变化(外部每次推送的都是当前完整内容)
|
|
|
|
|
|
watch(() => props.content, (newVal) => {
|
|
|
|
|
|
updateContent(newVal || '')
|
|
|
|
|
|
}, { immediate: true })
|
2025-11-19 00:12:47 +08:00
|
|
|
|
|
2025-11-22 18:30:02 +08:00
|
|
|
|
// 当流式开始时清空,防止旧内容残留
|
2025-11-10 23:53:05 +08:00
|
|
|
|
watch(() => props.isStreaming, (newVal, oldVal) => {
|
2025-11-22 17:17:15 +08:00
|
|
|
|
if (newVal && !oldVal) {
|
2025-11-22 18:30:02 +08:00
|
|
|
|
internalContent.value = ''
|
2025-11-22 17:17:15 +08:00
|
|
|
|
renderedContent.value = ''
|
|
|
|
|
|
}
|
2025-11-10 23:53:05 +08:00
|
|
|
|
})
|
2025-11-13 01:06:28 +08:00
|
|
|
|
|
2025-11-23 01:40:59 +08:00
|
|
|
|
// 监听页面可见性变化,避免后台时累积重复片段
|
|
|
|
|
|
function handleVisibilityChange() {
|
|
|
|
|
|
const wasVisible = isPageVisible.value
|
|
|
|
|
|
isPageVisible.value = !document.hidden
|
|
|
|
|
|
|
|
|
|
|
|
if (!wasVisible && isPageVisible.value) {
|
|
|
|
|
|
// 页面从不可见变为可见:立即同步最新内容
|
|
|
|
|
|
console.log('[ChatMessageRenderer] 页面重新可见,同步最新内容')
|
|
|
|
|
|
// 使用外部传入的最新content进行同步,而不是累积的增量
|
|
|
|
|
|
if (props.content) {
|
|
|
|
|
|
internalContent.value = props.content
|
|
|
|
|
|
updateRendered()
|
|
|
|
|
|
}
|
|
|
|
|
|
} else if (wasVisible && !isPageVisible.value) {
|
|
|
|
|
|
console.log('[ChatMessageRenderer] 页面进入后台')
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
onMounted(() => {
|
|
|
|
|
|
// 初始状态
|
|
|
|
|
|
handleVisibilityChange()
|
|
|
|
|
|
// 添加监听器
|
|
|
|
|
|
document.addEventListener('visibilitychange', handleVisibilityChange)
|
|
|
|
|
|
})
|
|
|
|
|
|
|
2025-11-22 18:30:02 +08:00
|
|
|
|
onUnmounted(() => {
|
|
|
|
|
|
if (debounceTimer) clearTimeout(debounceTimer)
|
2025-11-23 01:40:59 +08:00
|
|
|
|
// 移除监听器
|
|
|
|
|
|
document.removeEventListener('visibilitychange', handleVisibilityChange)
|
2025-11-22 18:30:02 +08:00
|
|
|
|
})
|
2025-11-10 23:53:05 +08:00
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
2025-11-22 18:30:02 +08:00
|
|
|
|
.prompt-display {
|
|
|
|
|
|
line-height: 1.6;
|
2025-12-28 13:49:45 +08:00
|
|
|
|
color: var(--color-text);
|
|
|
|
|
|
font-size: 14px;
|
2025-11-10 23:53:05 +08:00
|
|
|
|
}
|
2025-11-22 18:30:02 +08:00
|
|
|
|
</style>
|