前端优化

This commit is contained in:
2025-11-22 18:30:02 +08:00
parent 307c90f93e
commit fee84ce822
8 changed files with 198 additions and 167 deletions

View File

@@ -3,7 +3,7 @@
</template>
<script setup>
import { ref, watch } from 'vue'
import { ref, watch, nextTick,onUnmounted } from 'vue'
import { renderMarkdown } from '@/utils/markdown'
const props = defineProps({
@@ -17,93 +17,94 @@ const props = defineProps({
}
})
// 当前渲染的内容(避免重复渲染)
const currentContent = ref('')
// 渲染的 HTML 内容
// 内部维护的纯文本内容(用于计算增量和渲染
const internalContent = ref('')
// 最终渲染的 HTML
const renderedContent = ref('')
/**
* 更新渲染内容
* 只有当内容真正改变时才更新,避免重复渲染
*/
function updateRenderedContent() {
const content = currentContent.value
let debounceTimer = null
// 避免重复渲染相同内容
if (content === renderedContent.value.replace(/<[^>]*>/g, '')) {
return
}
if (!content) {
renderedContent.value = ''
return
}
// 渲染 markdown 为 HTML
renderedContent.value = renderMarkdown(content)
}
/**
* 处理内容更新
* 流式渲染:需要拼接增量内容
*/
function handleContentUpdate(newContent) {
if (!newContent) {
currentContent.value = ''
updateRenderedContent()
return
}
// 流式模式下拼接增量内容
if (props.isStreaming) {
currentContent.value += newContent
// 仅在流式时计算并追加增量
function appendStreamingDelta(newFullContent) {
const prev = internalContent.value
if (newFullContent.startsWith(prev)) {
// 正常情况:新内容包含旧内容 → 只追加差值
const delta = newFullContent.slice(prev.length)
internalContent.value += delta
} else {
// 非流式模式下直接替换
currentContent.value = newContent
// 异常情况(如后端重发了不连续的内容),直接覆盖防止乱序
console.warn('[PromptDisplay] Streaming content out of order, forcing replace')
internalContent.value = newFullContent
}
updateRenderedContent()
}
// 监听 content 变化,使用防抖处理避免频繁更新
let updateTimeout = null
watch(() => props.content, (newContent) => {
// 清除之前的定时器
if (updateTimeout) {
clearTimeout(updateTimeout)
// 更新 Markdown 渲染
async function updateRendered() {
if (!internalContent.value) {
renderedContent.value = ''
return
}
// renderMarkdown 可能包含异步操作(如 highlight.js用 nextTick 确保 DOM 就绪
renderedContent.value = await renderMarkdown(internalContent.value)
}
// 主更新逻辑
function updateContent(newContent = '') {
if (props.isStreaming) {
appendStreamingDelta(newContent)
} else {
internalContent.value = newContent
}
// 延迟更新,避免流式传输时频繁更新导致的性能问题
updateTimeout = setTimeout(() => {
handleContentUpdate(newContent)
}, 50) // 50ms 防抖
})
// 流式直接渲染,非流式防抖(避免频繁完整替换导致光标跳动或闪烁)
if (props.isStreaming) {
updateRendered()
} else {
clearTimeout(debounceTimer)
debounceTimer = setTimeout(() => {
updateRendered()
}, 60) // 60ms 足够平滑且不卡顿
}
}
// 监听 isStreaming 变化
// 监听 content 变化(外部每次推送的都是当前完整内容)
watch(() => props.content, (newVal) => {
updateContent(newVal || '')
}, { immediate: true })
// 当流式开始时清空,防止旧内容残留
watch(() => props.isStreaming, (newVal, oldVal) => {
// 流式传输开始时,清空之前的内容
if (newVal && !oldVal) {
currentContent.value = ''
internalContent.value = ''
renderedContent.value = ''
}
// 流式传输结束时,确保显示完整内容
if (!newVal && oldVal && props.content) {
currentContent.value = props.content
updateRenderedContent()
}
})
// 立即渲染初始内容
handleContentUpdate(props.content)
// 可选:组件卸载时清理定时器
onUnmounted(() => {
if (debounceTimer) clearTimeout(debounceTimer)
})
</script>
<style scoped>
/* 修复 pre 标签撑开容器的问题 */
.prompt-display {
line-height: 1.6;
}
/* 代码块优化 */
.prompt-display :deep(pre) {
max-width: 100%;
overflow-x: auto;
word-break: break-word;
white-space: pre-wrap;
word-wrap: break-word;
word-break: break-all;
background: #282c34;
color: #abb2bf;
padding: 1em;
border-radius: 8px;
margin: 1em 0;
}
</style>
.prompt-display :deep(code) {
font-family: 'Fira Code', Menlo, Monaco, Consolas, 'Courier New', monospace;
}
</style>