feat(代码块): 集成代码运行功能

- 添加运行/停止按钮交互
- 显示 Pyodide 加载进度(Python)
- 集成执行结果展示组件
- 添加更多语言的 Prism 语法高亮支持
- 优化高亮失败的降级处理
This commit is contained in:
gaoziman 2025-12-21 03:20:37 +08:00
parent 70902a2541
commit 269fc798aa

View File

@ -1,10 +1,20 @@
'use client';
import { useEffect, useRef, useState, useMemo } from 'react';
import { Copy, Check, Eye, ChevronDown, ChevronUp } from 'lucide-react';
import { useState, useMemo, useCallback } from 'react';
import { Copy, Check, Eye, ChevronDown, ChevronUp, Play, Square, Loader2 } from 'lucide-react';
import { cn } from '@/lib/utils';
import Prism from 'prismjs';
import { HtmlPreviewModal } from '@/components/ui/HtmlPreviewModal';
import { CodeExecutionResult, PyodideLoading } from '@/components/features/CodeExecutionResult';
import {
executeCode,
stopExecution,
isRunnableLanguage,
isCodeExecutable,
getLanguageConfig,
setPyodideLoadingCallbacks,
type ExecutionResult,
} from '@/lib/code-runner';
import 'prismjs/components/prism-javascript';
import 'prismjs/components/prism-typescript';
import 'prismjs/components/prism-jsx';
@ -24,6 +34,10 @@ import 'prismjs/components/prism-markdown';
import 'prismjs/components/prism-css';
import 'prismjs/components/prism-scss';
import 'prismjs/components/prism-markup';
import 'prismjs/components/prism-kotlin';
import 'prismjs/components/prism-swift';
import 'prismjs/components/prism-ruby';
import 'prismjs/components/prism-php';
interface CodeBlockProps {
code: string;
@ -62,6 +76,15 @@ export function CodeBlock({
const [previewOpen, setPreviewOpen] = useState(false);
const [isExpanded, setIsExpanded] = useState(false);
// 代码执行相关状态
const [isRunning, setIsRunning] = useState(false);
const [executionResult, setExecutionResult] = useState<ExecutionResult | null>(null);
const [pyodideStatus, setPyodideStatus] = useState<{
stage: 'loading' | 'ready' | 'error';
message: string;
progress?: number;
} | null>(null);
// 规范化语言名称
const normalizedLanguage = languageAliases[language.toLowerCase()] || language.toLowerCase();
@ -69,6 +92,10 @@ export function CodeBlock({
const isHtmlPreviewable = ['html', 'htm', 'markup'].includes(normalizedLanguage) ||
['html', 'htm'].includes(language.toLowerCase());
// 判断是否可执行:语言支持 + 代码满足执行条件
const canRun = isRunnableLanguage(language) && isCodeExecutable(code, language);
const languageConfig = canRun ? getLanguageConfig(language) : null;
const lines = code.split('\n');
const totalLines = lines.length;
const shouldCollapse = totalLines > maxCollapsedLines;
@ -86,11 +113,20 @@ export function CodeBlock({
// 使用 useMemo 缓存高亮后的 HTML避免频繁重新高亮
const highlightedCode = useMemo(() => {
try {
const grammar = Prism.languages[normalizedLanguage];
if (grammar) {
return Prism.highlight(displayCode, grammar, normalizedLanguage);
}
return displayCode;
} catch (error) {
// 某些语言的 grammar 可能不完整fallback 到纯文本
console.warn(`Prism highlight failed for ${normalizedLanguage}:`, error);
}
// 对纯文本进行 HTML 转义
return displayCode
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
}, [displayCode, normalizedLanguage]);
const handleCopy = async () => {
@ -108,6 +144,67 @@ export function CodeBlock({
setIsExpanded(!isExpanded);
};
// 运行代码
const handleRun = useCallback(async () => {
if (isRunning || !canRun) return;
setIsRunning(true);
setExecutionResult(null);
// 如果是 Python设置 Pyodide 加载回调
if (languageConfig?.engine === 'pyodide') {
setPyodideLoadingCallbacks({
onLoadingStart: () => {
setPyodideStatus({
stage: 'loading',
message: '正在加载 Python 运行时...',
progress: 0,
});
},
onLoadingProgress: (message, progress) => {
setPyodideStatus({
stage: 'loading',
message,
progress,
});
},
onLoadingComplete: () => {
setPyodideStatus(null);
},
onLoadingError: (error) => {
setPyodideStatus({
stage: 'error',
message: error,
});
},
});
}
try {
const result = await executeCode(code, language);
setExecutionResult(result);
} catch (error) {
setExecutionResult({
success: false,
output: '',
error: error instanceof Error ? error.message : '执行失败',
executionTime: 0,
engine: languageConfig?.engine || 'sandbox',
});
} finally {
setIsRunning(false);
setPyodideStatus(null);
setPyodideLoadingCallbacks(null);
}
}, [code, language, isRunning, canRun, languageConfig]);
// 停止执行
const handleStop = useCallback(() => {
stopExecution();
setIsRunning(false);
setPyodideStatus(null);
}, []);
return (
<div className={cn('relative group rounded overflow-hidden my-4', className)}
style={{ boxShadow: 'var(--color-code-shadow)' }}>
@ -141,6 +238,35 @@ export function CodeBlock({
{/* 右侧:操作按钮 */}
<div className="flex items-center gap-2">
{/* 运行按钮 */}
{canRun && (
<button
onClick={isRunning ? handleStop : handleRun}
disabled={!canRun}
className={cn(
'inline-flex items-center gap-1.5 px-2 py-1 rounded transition-colors',
isRunning
? 'hover:bg-red-500/20'
: 'hover:bg-white/10'
)}
style={{
color: isRunning ? '#ef4444' : 'var(--color-primary)',
}}
title={isRunning ? '停止执行' : `运行 ${languageConfig?.label || language}`}
>
{isRunning ? (
<>
<Square size={14} />
<span></span>
</>
) : (
<>
<Play size={14} />
<span></span>
</>
)}
</button>
)}
{/* HTML 预览按钮 */}
{isHtmlPreviewable && (
<button
@ -265,6 +391,47 @@ export function CodeBlock({
)}
</div>
{/* Pyodide 加载状态Python */}
{pyodideStatus && (
<PyodideLoading
stage={pyodideStatus.stage}
message={pyodideStatus.message}
progress={pyodideStatus.progress}
/>
)}
{/* 代码执行结果 */}
{(executionResult || isRunning) && (
<div className="px-0">
{isRunning && !pyodideStatus ? (
<div
className="mt-2 rounded border overflow-hidden"
style={{
backgroundColor: 'var(--color-code-bg)',
borderColor: 'var(--color-code-border)',
}}
>
<div
className="px-4 py-3 flex items-center gap-2"
style={{ color: 'var(--color-code-toolbar-text)' }}
>
<Loader2 size={16} className="animate-spin" style={{ color: 'var(--color-primary)' }} />
<span className="text-sm"> {languageConfig?.label || language} ...</span>
</div>
</div>
) : executionResult && (
<CodeExecutionResult
output={executionResult.output}
error={executionResult.error}
language={languageConfig?.label || language}
engine={executionResult.engine}
executionTime={executionResult.executionTime}
success={executionResult.success}
/>
)}
</div>
)}
{/* HTML 预览模态框 */}
{isHtmlPreviewable && (
<HtmlPreviewModal