feat: 优化

This commit is contained in:
2026-01-18 18:36:37 +08:00
parent 265ee3a453
commit f5bccf8da4
11 changed files with 1435 additions and 252 deletions

View File

@@ -0,0 +1,142 @@
<template>
<div class="w-full h-full">
<!-- 主渲染区域 -->
<div
:class="{ 'streaming': isStreaming }"
>
<!-- 消息渲染器 - GitHub 风格 -->
<div
class="markdown-body"
:class="{ 'streaming': isStreaming }"
v-html="renderedContent"
/>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, watch, onUnmounted, onMounted } from 'vue'
import { renderMarkdown } from '@/utils/markdown'
import type { ChatMessageRendererV2Props } from '@/services/ai-bridge'
import 'github-markdown-css/github-markdown.css'
const props = withDefaults(defineProps<ChatMessageRendererV2Props>(), {
config: () => ({
enableTypewriter: true,
typewriterSpeed: 10,
enableMarkdown: true,
enableHighlighting: true
})
})
const emit = defineEmits<{
'stream-start': []
'stream-chunk': [chunk: string]
'stream-end': [fullContent: string]
'error': [error: Error]
}>()
const internalContent = ref('')
const renderedContent = ref('')
const isPageVisible = ref(true)
watch(() => props.content, (newContent) => {
if (newContent !== undefined && newContent !== null) {
updateContent(newContent)
}
}, { immediate: true })
watch(() => props.isStreaming, (newValue, oldValue) => {
if (newValue && !oldValue) {
handleStreamStart()
} else if (!newValue && oldValue) {
handleStreamEnd()
}
})
async function updateContent(newContent = '') {
if (props.isStreaming && internalContent.value) {
await updateStreamingContent(newContent)
} else {
await updateStaticContent(newContent)
}
}
async function updateStreamingContent(newFullContent: string) {
const prev = internalContent.value
let delta: string
if (newFullContent.startsWith(prev)) {
delta = newFullContent.slice(prev.length)
} else {
console.warn('[ChatMessageRendererV2] 流式内容乱序,使用完整内容')
delta = newFullContent
internalContent.value = ''
}
internalContent.value += delta
emit('stream-chunk', delta)
await renderContent(internalContent.value)
}
async function updateStaticContent(newContent: string) {
internalContent.value = newContent
await renderContent(newContent)
}
async function renderContent(content: string) {
if (!content) {
renderedContent.value = ''
return
}
try {
if (props.config?.enableMarkdown !== false) {
renderedContent.value = await renderMarkdown(content)
} else {
renderedContent.value = content.replace(/\n/g, '<br>')
}
} catch (error) {
console.error('[ChatMessageRendererV2] 渲染错误:', error)
renderedContent.value = content
}
}
function handleStreamStart() {
internalContent.value = ''
renderedContent.value = ''
emit('stream-start')
}
function handleStreamEnd() {
emit('stream-end', internalContent.value)
}
function handleVisibilityChange() {
const wasVisible = isPageVisible.value
isPageVisible.value = !document.hidden
if (!wasVisible && isPageVisible.value && props.content) {
updateContent(props.content)
}
}
onMounted(() => {
document.addEventListener('visibilitychange', handleVisibilityChange)
handleVisibilityChange()
})
onUnmounted(() => {
document.removeEventListener('visibilitychange', handleVisibilityChange)
})
defineExpose({
reset: () => {
internalContent.value = ''
renderedContent.value = ''
}
})
</script>
<style scoped lang="less">
</style>