feat(代码块): 添加长代码折叠/展开功能
- 超过60行代码自动折叠,预览前30行 - 添加渐变遮罩和展开按钮交互 - 显示总行数和剩余行数信息 - 展开状态下显示收起按钮 - 复制功能始终复制完整代码 - 优化代码区域布局使用 flex 结构
This commit is contained in:
parent
4eab17155e
commit
b3d5a47072
@ -1,7 +1,7 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useRef, useState, useMemo } from 'react';
|
||||
import { Copy, Check, Eye } from 'lucide-react';
|
||||
import { Copy, Check, Eye, ChevronDown, ChevronUp } from 'lucide-react';
|
||||
import { cn } from '@/lib/utils';
|
||||
import Prism from 'prismjs';
|
||||
import { HtmlPreviewModal } from '@/components/ui/HtmlPreviewModal';
|
||||
@ -30,6 +30,10 @@ interface CodeBlockProps {
|
||||
language?: string;
|
||||
showLineNumbers?: boolean;
|
||||
className?: string;
|
||||
/** 折叠阈值,超过此行数自动折叠,默认 60 */
|
||||
maxCollapsedLines?: number;
|
||||
/** 折叠时预览的行数,默认 30 */
|
||||
previewLines?: number;
|
||||
}
|
||||
|
||||
// 语言别名映射
|
||||
@ -51,9 +55,12 @@ export function CodeBlock({
|
||||
language = 'text',
|
||||
showLineNumbers = true,
|
||||
className,
|
||||
maxCollapsedLines = 60,
|
||||
previewLines = 30,
|
||||
}: CodeBlockProps) {
|
||||
const [copied, setCopied] = useState(false);
|
||||
const [previewOpen, setPreviewOpen] = useState(false);
|
||||
const [isExpanded, setIsExpanded] = useState(false);
|
||||
|
||||
// 规范化语言名称
|
||||
const normalizedLanguage = languageAliases[language.toLowerCase()] || language.toLowerCase();
|
||||
@ -62,17 +69,33 @@ export function CodeBlock({
|
||||
const isHtmlPreviewable = ['html', 'htm', 'markup'].includes(normalizedLanguage) ||
|
||||
['html', 'htm'].includes(language.toLowerCase());
|
||||
|
||||
const lines = code.split('\n');
|
||||
const totalLines = lines.length;
|
||||
const shouldCollapse = totalLines > maxCollapsedLines;
|
||||
const remainingLines = totalLines - previewLines;
|
||||
|
||||
// 根据折叠状态获取显示的代码
|
||||
const displayCode = useMemo(() => {
|
||||
if (shouldCollapse && !isExpanded) {
|
||||
return lines.slice(0, previewLines).join('\n');
|
||||
}
|
||||
return code;
|
||||
}, [code, lines, shouldCollapse, isExpanded, previewLines]);
|
||||
|
||||
const displayLines = displayCode.split('\n');
|
||||
|
||||
// 使用 useMemo 缓存高亮后的 HTML,避免频繁重新高亮
|
||||
const highlightedCode = useMemo(() => {
|
||||
const grammar = Prism.languages[normalizedLanguage];
|
||||
if (grammar) {
|
||||
return Prism.highlight(code, grammar, normalizedLanguage);
|
||||
return Prism.highlight(displayCode, grammar, normalizedLanguage);
|
||||
}
|
||||
return code;
|
||||
}, [code, normalizedLanguage]);
|
||||
return displayCode;
|
||||
}, [displayCode, normalizedLanguage]);
|
||||
|
||||
const handleCopy = async () => {
|
||||
try {
|
||||
// 始终复制完整代码
|
||||
await navigator.clipboard.writeText(code);
|
||||
setCopied(true);
|
||||
setTimeout(() => setCopied(false), 2000);
|
||||
@ -81,13 +104,15 @@ export function CodeBlock({
|
||||
}
|
||||
};
|
||||
|
||||
const lines = code.split('\n');
|
||||
const toggleExpand = () => {
|
||||
setIsExpanded(!isExpanded);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={cn('relative group rounded overflow-hidden my-4', className)}
|
||||
style={{ boxShadow: 'var(--color-code-shadow)' }}>
|
||||
{/* 顶部工具栏 */}
|
||||
<div className="flex items-center justify-between px-4 py-2.5 text-sm"
|
||||
<div className="flex items-center justify-between px-4 py-2.5 text-[0.9em]"
|
||||
style={{
|
||||
backgroundColor: 'var(--color-code-toolbar-bg)',
|
||||
color: 'var(--color-code-toolbar-text)',
|
||||
@ -106,6 +131,12 @@ export function CodeBlock({
|
||||
style={{ backgroundColor: '#27c93f', boxShadow: '0 0 0 1px rgba(0, 0, 0, 0.1) inset' }} />
|
||||
</div>
|
||||
<span className="font-mono text-[13px]">{language || 'code'}</span>
|
||||
{/* 显示总行数 */}
|
||||
{shouldCollapse && (
|
||||
<span className="text-[12px] opacity-60">
|
||||
({totalLines} 行)
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 右侧:操作按钮 */}
|
||||
@ -145,40 +176,93 @@ export function CodeBlock({
|
||||
</div>
|
||||
|
||||
{/* 代码区域 */}
|
||||
<div className="relative overflow-x-auto"
|
||||
style={{ backgroundColor: 'var(--color-code-bg)' }}>
|
||||
{showLineNumbers && (
|
||||
<div className="absolute left-0 top-0 bottom-0 w-12 select-none"
|
||||
style={{
|
||||
backgroundColor: 'var(--color-code-bg)',
|
||||
borderRight: '1px solid var(--color-code-line-border)'
|
||||
}}>
|
||||
<div className="py-4 px-2 text-right">
|
||||
{lines.map((_, index) => (
|
||||
<div style={{ backgroundColor: 'var(--color-code-bg)' }}>
|
||||
{/* 代码内容 - 使用 flex 布局 */}
|
||||
<div className="flex overflow-x-auto">
|
||||
{/* 行号列 */}
|
||||
{showLineNumbers && (
|
||||
<div
|
||||
className="flex-shrink-0 w-12 select-none py-4 px-2 text-right"
|
||||
style={{
|
||||
backgroundColor: 'var(--color-code-bg)',
|
||||
borderRight: '1px solid var(--color-code-line-border)'
|
||||
}}
|
||||
>
|
||||
{displayLines.map((_, index) => (
|
||||
<div
|
||||
key={index}
|
||||
className="text-sm font-mono leading-6"
|
||||
className="font-mono leading-6"
|
||||
style={{ color: 'var(--color-code-line-number)' }}
|
||||
>
|
||||
{index + 1}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{/* 代码列 */}
|
||||
<pre
|
||||
className="flex-1 p-4 overflow-x-auto"
|
||||
style={{ margin: 0, background: 'transparent' }}
|
||||
>
|
||||
<code
|
||||
className={`language-${normalizedLanguage} leading-6`}
|
||||
style={{ color: 'var(--color-code-text)' }}
|
||||
dangerouslySetInnerHTML={{ __html: highlightedCode }}
|
||||
/>
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
{/* 渐变遮罩和展开按钮(仅在折叠状态下显示) */}
|
||||
{shouldCollapse && !isExpanded && (
|
||||
<>
|
||||
{/* 渐变遮罩 */}
|
||||
<div
|
||||
className="h-16 -mt-16 pointer-events-none relative z-10"
|
||||
style={{
|
||||
background: `linear-gradient(to bottom, transparent, var(--color-code-bg))`
|
||||
}}
|
||||
/>
|
||||
{/* 展开按钮 */}
|
||||
<div
|
||||
className="py-3 text-center cursor-pointer transition-colors hover:opacity-80"
|
||||
style={{ backgroundColor: 'var(--color-code-bg)' }}
|
||||
onClick={toggleExpand}
|
||||
>
|
||||
<button
|
||||
className="inline-flex items-center gap-2 px-4 py-2 rounded-lg text-[0.9em] font-medium transition-colors"
|
||||
style={{
|
||||
color: 'var(--color-primary)',
|
||||
backgroundColor: 'var(--color-primary-alpha)'
|
||||
}}
|
||||
>
|
||||
<ChevronDown size={16} />
|
||||
<span>展开剩余 {remainingLines} 行代码</span>
|
||||
</button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* 收起按钮(仅在展开状态下显示) */}
|
||||
{shouldCollapse && isExpanded && (
|
||||
<div
|
||||
className="py-3 text-center cursor-pointer transition-colors"
|
||||
style={{
|
||||
backgroundColor: 'var(--color-code-bg)',
|
||||
borderTop: '1px solid var(--color-code-line-border)'
|
||||
}}
|
||||
onClick={toggleExpand}
|
||||
>
|
||||
<button
|
||||
className="inline-flex items-center gap-2 px-4 py-2 rounded-lg text-[0.9em] font-medium transition-colors"
|
||||
style={{
|
||||
color: 'var(--color-code-toolbar-text)'
|
||||
}}
|
||||
>
|
||||
<ChevronUp size={16} />
|
||||
<span>收起代码</span>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
<pre
|
||||
className={cn(
|
||||
'p-4 overflow-x-auto',
|
||||
showLineNumbers && 'pl-16'
|
||||
)}
|
||||
style={{ margin: 0, background: 'transparent' }}
|
||||
>
|
||||
<code
|
||||
className={`language-${normalizedLanguage} text-sm leading-6`}
|
||||
style={{ color: 'var(--color-code-text)' }}
|
||||
dangerouslySetInnerHTML={{ __html: highlightedCode }}
|
||||
/>
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
{/* HTML 预览模态框 */}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user