send-stream
This commit is contained in:
@@ -3,7 +3,7 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, watch, onUnmounted } from 'vue'
|
||||
import { ref, watch } from 'vue'
|
||||
import { renderMarkdown } from '@/utils/markdown'
|
||||
|
||||
const props = defineProps({
|
||||
@@ -17,152 +17,73 @@ const props = defineProps({
|
||||
}
|
||||
})
|
||||
|
||||
// 当前显示的内容(用于打字机效果)
|
||||
const displayedContent = ref('')
|
||||
// 目标内容(完整内容)
|
||||
const targetContent = ref('')
|
||||
// 动画帧 ID
|
||||
let animationFrameId = null
|
||||
// 打字机速度(字符/帧,可根据性能调整)
|
||||
const TYPING_SPEED = 3
|
||||
// 是否正在执行打字机动画
|
||||
const isTyping = ref(false)
|
||||
|
||||
/**
|
||||
* 高性能打字机效果渲染
|
||||
* 使用 requestAnimationFrame 实现平滑的逐字符显示
|
||||
*/
|
||||
function typewriterEffect() {
|
||||
if (!isTyping.value) return
|
||||
|
||||
const currentLength = displayedContent.value.length
|
||||
const targetLength = targetContent.value.length
|
||||
|
||||
if (currentLength >= targetLength) {
|
||||
// 已完成,停止动画
|
||||
isTyping.value = false
|
||||
displayedContent.value = targetContent.value
|
||||
return
|
||||
}
|
||||
|
||||
// 每次增加多个字符以提高性能
|
||||
const nextLength = Math.min(currentLength + TYPING_SPEED, targetLength)
|
||||
displayedContent.value = targetContent.value.slice(0, nextLength)
|
||||
|
||||
// 继续下一帧
|
||||
animationFrameId = requestAnimationFrame(typewriterEffect)
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始打字机效果
|
||||
*/
|
||||
function startTypewriter() {
|
||||
if (animationFrameId) {
|
||||
cancelAnimationFrame(animationFrameId)
|
||||
}
|
||||
|
||||
isTyping.value = true
|
||||
animationFrameId = requestAnimationFrame(typewriterEffect)
|
||||
}
|
||||
|
||||
/**
|
||||
* 停止打字机效果
|
||||
*/
|
||||
function stopTypewriter() {
|
||||
if (animationFrameId) {
|
||||
cancelAnimationFrame(animationFrameId)
|
||||
animationFrameId = null
|
||||
}
|
||||
isTyping.value = false
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算渲染的 HTML 内容
|
||||
*/
|
||||
// 当前渲染的内容(避免重复渲染)
|
||||
const currentContent = ref('')
|
||||
// 渲染的 HTML 内容
|
||||
const renderedContent = ref('')
|
||||
|
||||
/**
|
||||
* 更新渲染内容
|
||||
* 只有当内容真正改变时才更新,避免重复渲染
|
||||
*/
|
||||
function updateRenderedContent() {
|
||||
const content = displayedContent.value
|
||||
const content = currentContent.value
|
||||
|
||||
// 避免重复渲染相同内容
|
||||
if (content === renderedContent.value.replace(/<[^>]*>/g, '')) {
|
||||
return
|
||||
}
|
||||
|
||||
if (!content) {
|
||||
renderedContent.value = ''
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
// 渲染 markdown 为 HTML
|
||||
renderedContent.value = renderMarkdown(content)
|
||||
}
|
||||
|
||||
// 监听 displayedContent 变化,更新渲染
|
||||
watch(displayedContent, () => {
|
||||
updateRenderedContent()
|
||||
}, { immediate: true })
|
||||
|
||||
/**
|
||||
* 处理内容更新
|
||||
* 普通流式渲染:直接显示所有内容,不使用打字机效果
|
||||
*/
|
||||
function handleContentUpdate(newContent) {
|
||||
if (!newContent) {
|
||||
targetContent.value = ''
|
||||
displayedContent.value = ''
|
||||
stopTypewriter()
|
||||
currentContent.value = ''
|
||||
updateRenderedContent()
|
||||
return
|
||||
}
|
||||
|
||||
// 更新目标内容
|
||||
targetContent.value = newContent
|
||||
|
||||
if (props.isStreaming) {
|
||||
// 流式传输模式:使用打字机效果显示内容
|
||||
// 流式传输时,内容会逐步到达,使用打字机效果增强体验
|
||||
const currentLength = displayedContent.value.length
|
||||
const newLength = newContent.length
|
||||
|
||||
if (newLength !== currentLength) {
|
||||
// 内容发生变化
|
||||
if (newLength < currentLength) {
|
||||
// 内容被重置或缩短,直接显示新内容
|
||||
displayedContent.value = newContent
|
||||
stopTypewriter()
|
||||
} else {
|
||||
// 内容增加,使用打字机效果显示新增部分
|
||||
if (!isTyping.value) {
|
||||
startTypewriter()
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 静态内容模式:直接显示全部内容,不使用打字机效果
|
||||
displayedContent.value = newContent
|
||||
stopTypewriter()
|
||||
}
|
||||
|
||||
// 更新当前内容
|
||||
currentContent.value = newContent
|
||||
updateRenderedContent()
|
||||
}
|
||||
|
||||
// 监听 content 变化
|
||||
// 监听 content 变化,使用防抖处理避免频繁更新
|
||||
let updateTimeout = null
|
||||
watch(() => props.content, (newContent) => {
|
||||
handleContentUpdate(newContent)
|
||||
}, { immediate: true })
|
||||
// 清除之前的定时器
|
||||
if (updateTimeout) {
|
||||
clearTimeout(updateTimeout)
|
||||
}
|
||||
|
||||
// 延迟更新,避免流式传输时频繁更新导致的性能问题
|
||||
updateTimeout = setTimeout(() => {
|
||||
handleContentUpdate(newContent)
|
||||
}, 50) // 50ms 防抖
|
||||
})
|
||||
|
||||
// 监听 isStreaming 变化
|
||||
watch(() => props.isStreaming, (newVal, oldVal) => {
|
||||
if (!newVal && oldVal) {
|
||||
// 流式传输结束,确保显示完整内容,停止打字机效果
|
||||
if (targetContent.value) {
|
||||
displayedContent.value = targetContent.value
|
||||
}
|
||||
stopTypewriter()
|
||||
} else if (newVal) {
|
||||
// 开始流式传输,如果内容有变化,启动打字机效果
|
||||
// 打字机效果会在 handleContentUpdate 中根据内容变化自动启动
|
||||
// 流式传输结束时,确保显示完整内容
|
||||
if (!newVal && oldVal && props.content) {
|
||||
currentContent.value = props.content
|
||||
updateRenderedContent()
|
||||
}
|
||||
})
|
||||
|
||||
// 组件卸载时清理
|
||||
onUnmounted(() => {
|
||||
stopTypewriter()
|
||||
})
|
||||
// 立即渲染初始内容
|
||||
handleContentUpdate(props.content)
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
|
||||
Reference in New Issue
Block a user