agent-Specialization/static/src/components/chat/MarkdownRenderer.vue
JOJO 737c4d61a2 fix(chat): 修复流式代码块闪烁、复制错位及数学公式实时多行渲染
- 将聊天消息中的代码块提取为独立 Vue 组件,稳定 key 避免流式输出时重建

- 代码块复制按钮直接复制当前组件内容,全局复制逻辑改为按容器查找

- 预处理 LaTeX 公式占位符并在组件 onUpdated 中实时渲染,支持多行 49888...49888

- 公式块过长时提供横向滚动,渲染后主动追底避免滚动锁定失效
2026-06-27 23:11:09 +08:00

75 lines
1.6 KiB
Vue

<template>
<div ref="containerRef" class="markdown-renderer">
<template v-for="segment in segments" :key="segment.key">
<div
v-if="segment.type === 'text'"
class="markdown-text-segment"
v-html="renderText(segment.content)"
></div>
<CodeBlock
v-else
:content="segment.content"
:language="segment.language"
:is-streaming="!segment.closed"
/>
</template>
</div>
</template>
<script setup lang="ts">
import { computed, inject, nextTick, onMounted, onUpdated, ref, watch } from 'vue';
import CodeBlock from './CodeBlock.vue';
import {
parseMarkdownSegments,
renderMarkdownText,
renderMathBlocks
} from '@/composables/useMarkdownRenderer';
defineOptions({ name: 'MarkdownRenderer' });
const props = defineProps<{
content: string;
isStreaming?: boolean;
}>();
const containerRef = ref<HTMLElement | null>(null);
const onMathRendered = inject<() => void>('mathRenderedCallback', () => {});
const segments = computed(() => parseMarkdownSegments(props.content || '', props.isStreaming));
function renderText(text: string) {
return renderMarkdownText(text);
}
function renderMath() {
nextTick(() => {
if (containerRef.value) {
renderMathBlocks(containerRef.value);
onMathRendered();
}
});
}
onMounted(renderMath);
onUpdated(renderMath);
watch(() => props.content, renderMath, { immediate: true });
</script>
<style scoped>
.markdown-renderer {
display: contents;
}
.markdown-text-segment {
display: block;
}
.markdown-text-segment > *:first-child {
margin-top: 0;
}
.markdown-text-segment > *:last-child {
margin-bottom: 0;
}
</style>