fix(ui): CodeBlock 添加防抖优化,减少流式输出时的高亮闪烁
- 使用 ref 跟踪上一次高亮的代码,避免重复高亮 - 添加 150ms 防抖延迟,减少流式输出时的高亮次数 - 优化渲染逻辑,未高亮时显示纯文本
This commit is contained in:
@@ -4,7 +4,7 @@
|
|||||||
* 代码块组件,支持语法高亮和一键复制
|
* 代码块组件,支持语法高亮和一键复制
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { useState, useEffect } from 'react';
|
import { useState, useEffect, useRef } from 'react';
|
||||||
import { Copy, Check } from 'lucide-react';
|
import { Copy, Check } from 'lucide-react';
|
||||||
import { codeToHtml } from 'shiki';
|
import { codeToHtml } from 'shiki';
|
||||||
import { cn } from '../utils/cn';
|
import { cn } from '../utils/cn';
|
||||||
@@ -18,37 +18,42 @@ interface CodeBlockProps {
|
|||||||
export function CodeBlock({ code, language = 'text', className }: CodeBlockProps) {
|
export function CodeBlock({ code, language = 'text', className }: CodeBlockProps) {
|
||||||
const [copied, setCopied] = useState(false);
|
const [copied, setCopied] = useState(false);
|
||||||
const [highlightedHtml, setHighlightedHtml] = useState<string>('');
|
const [highlightedHtml, setHighlightedHtml] = useState<string>('');
|
||||||
const [isLoading, setIsLoading] = useState(true);
|
// 使用 ref 跟踪上一次高亮的代码,避免不必要的重新高亮
|
||||||
|
const lastHighlightedCode = useRef<string>('');
|
||||||
|
const highlightTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||||
|
|
||||||
// 语法高亮
|
// 语法高亮 - 使用防抖避免频繁高亮导致闪烁
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let cancelled = false;
|
// 清除之前的定时器
|
||||||
|
if (highlightTimer.current) {
|
||||||
|
clearTimeout(highlightTimer.current);
|
||||||
|
}
|
||||||
|
|
||||||
async function highlight() {
|
// 如果代码没变,不需要重新高亮
|
||||||
|
if (code === lastHighlightedCode.current && highlightedHtml) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 使用防抖:等待 150ms 后再进行高亮(流式输出时减少高亮次数)
|
||||||
|
highlightTimer.current = setTimeout(async () => {
|
||||||
try {
|
try {
|
||||||
const html = await codeToHtml(code, {
|
const html = await codeToHtml(code, {
|
||||||
lang: language,
|
lang: language,
|
||||||
theme: 'github-dark',
|
theme: 'github-dark',
|
||||||
});
|
});
|
||||||
if (!cancelled) {
|
lastHighlightedCode.current = code;
|
||||||
setHighlightedHtml(html);
|
setHighlightedHtml(html);
|
||||||
}
|
|
||||||
} catch {
|
} catch {
|
||||||
// 如果语言不支持,回退到纯文本
|
// 如果语言不支持,回退到纯文本
|
||||||
if (!cancelled) {
|
lastHighlightedCode.current = code;
|
||||||
setHighlightedHtml(`<pre><code>${escapeHtml(code)}</code></pre>`);
|
setHighlightedHtml(`<pre><code>${escapeHtml(code)}</code></pre>`);
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
if (!cancelled) {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}, 150);
|
||||||
|
|
||||||
highlight();
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
cancelled = true;
|
if (highlightTimer.current) {
|
||||||
|
clearTimeout(highlightTimer.current);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
}, [code, language]);
|
}, [code, language]);
|
||||||
|
|
||||||
@@ -82,17 +87,17 @@ export function CodeBlock({ code, language = 'text', className }: CodeBlockProps
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* 代码内容 */}
|
{/* 代码内容 - 始终显示高亮后的内容,未高亮时显示纯文本 */}
|
||||||
<div className="overflow-x-auto">
|
<div className="overflow-x-auto">
|
||||||
{isLoading ? (
|
{highlightedHtml ? (
|
||||||
<pre className="p-4 bg-code text-fg-secondary text-sm font-mono">
|
|
||||||
<code>{code}</code>
|
|
||||||
</pre>
|
|
||||||
) : (
|
|
||||||
<div
|
<div
|
||||||
className="shiki-wrapper text-sm [&>pre]:p-4 [&>pre]:m-0 [&>pre]:bg-code"
|
className="shiki-wrapper text-sm [&>pre]:p-4 [&>pre]:m-0 [&>pre]:bg-code"
|
||||||
dangerouslySetInnerHTML={{ __html: highlightedHtml }}
|
dangerouslySetInnerHTML={{ __html: highlightedHtml }}
|
||||||
/>
|
/>
|
||||||
|
) : (
|
||||||
|
<pre className="p-4 bg-code text-fg-secondary text-sm font-mono">
|
||||||
|
<code>{code}</code>
|
||||||
|
</pre>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user