- 将聊天消息中的代码块提取为独立 Vue 组件,稳定 key 避免流式输出时重建 - 代码块复制按钮直接复制当前组件内容,全局复制逻辑改为按容器查找 - 预处理 LaTeX 公式占位符并在组件 onUpdated 中实时渲染,支持多行 49888...49888 - 公式块过长时提供横向滚动,渲染后主动追底避免滚动锁定失效
75 lines
1.6 KiB
Vue
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>
|