feat: 优化
This commit is contained in:
142
frontend/app/web-gold/src/components/ChatMessageRendererV2.vue
Normal file
142
frontend/app/web-gold/src/components/ChatMessageRendererV2.vue
Normal 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>
|
||||
Reference in New Issue
Block a user