| |
| |
| |
| |
| import { useState, useRef, useEffect } from 'react'; |
| import { Button, Space, Input, Typography } from 'antd'; |
| import { |
| PlayCircleOutlined, |
| StopOutlined, |
| ClearOutlined, |
| CodeOutlined |
| } from '@ant-design/icons'; |
| import CodeMirror from '@uiw/react-codemirror'; |
| import { python } from '@codemirror/lang-python'; |
| import api from '../../services/api'; |
|
|
| const { Text } = Typography; |
|
|
| const CodeExecutor = ({ initialCode = '' }) => { |
| const [code, setCode] = useState(initialCode); |
| const [output, setOutput] = useState([]); |
| const [isRunning, setIsRunning] = useState(false); |
| const [needsInput, setNeedsInput] = useState(false); |
| const [contextId, setContextId] = useState(null); |
| const [userInput, setUserInput] = useState(''); |
| const outputEndRef = useRef(null); |
|
|
| useEffect(() => { |
| if (initialCode) setCode(initialCode); |
| }, [initialCode]); |
|
|
| useEffect(() => { |
| outputEndRef.current?.scrollIntoView({ behavior: 'smooth' }); |
| }, [output]); |
|
|
| const appendOutput = (type, text) => { |
| setOutput(prev => [...prev, { type, text }]); |
| }; |
|
|
| const handleRun = async () => { |
| if (!code.trim()) return; |
| setIsRunning(true); |
| setNeedsInput(false); |
| setOutput([{ type: 'info', text: '>>> 运行中...' }]); |
|
|
| try { |
| const res = await api.post('/code/execute', { code }); |
|
|
| if (!res.success) { |
| appendOutput('error', res.error || '执行失败'); |
| if (res.traceback) appendOutput('error', res.traceback); |
| setIsRunning(false); |
| return; |
| } |
|
|
| |
| if (res.output) { |
| appendOutput('stdout', res.output); |
| } |
|
|
| if (res.needsInput) { |
| |
| setContextId(res.context_id); |
| setNeedsInput(true); |
| } else { |
| |
| appendOutput('info', '>>> 执行完成'); |
| setIsRunning(false); |
| } |
| } catch (error) { |
| appendOutput('error', `错误: ${error.message}`); |
| setIsRunning(false); |
| } |
| }; |
|
|
| const handleInput = async () => { |
| if (!contextId) return; |
|
|
| const inputText = userInput; |
| appendOutput('input', `<<< ${inputText}`); |
| setUserInput(''); |
| setNeedsInput(false); |
|
|
| try { |
| const res = await api.post('/code/input', { |
| context_id: contextId, |
| input: inputText |
| }); |
|
|
| if (!res.success) { |
| appendOutput('error', res.error || '输入处理失败'); |
| if (res.traceback) appendOutput('error', res.traceback); |
| setIsRunning(false); |
| setContextId(null); |
| return; |
| } |
|
|
| if (res.output) { |
| appendOutput('stdout', res.output); |
| } |
|
|
| if (res.needsInput) { |
| |
| setNeedsInput(true); |
| } else { |
| |
| appendOutput('info', '>>> 执行完成'); |
| setIsRunning(false); |
| setContextId(null); |
| } |
| } catch (error) { |
| appendOutput('error', `错误: ${error.message}`); |
| setIsRunning(false); |
| setContextId(null); |
| } |
| }; |
|
|
| const handleStop = async () => { |
| if (contextId) { |
| try { |
| const res = await api.post('/code/stop', { context_id: contextId }); |
| if (res.output) { |
| appendOutput('stdout', res.output); |
| } |
| } catch (e) { } |
| } |
| appendOutput('warning', '>>> 已终止'); |
| setIsRunning(false); |
| setNeedsInput(false); |
| setContextId(null); |
| }; |
|
|
| const handleClear = () => { |
| setOutput([]); |
| }; |
|
|
| const getOutputColor = (type) => { |
| switch (type) { |
| case 'error': return '#ef4444'; |
| case 'warning': return '#f59e0b'; |
| case 'input': return '#10b981'; |
| case 'info': return '#6b7280'; |
| default: return '#e5e7eb'; |
| } |
| }; |
|
|
| return ( |
| <div style={{ display: 'flex', flexDirection: 'column', height: '100%', gap: '8px' }}> |
| {/* 工具栏 */} |
| <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}> |
| <Text strong style={{ fontSize: '13px' }}> |
| <CodeOutlined /> Python 编辑器 |
| </Text> |
| <Space size="small"> |
| {!isRunning ? ( |
| <Button |
| type="primary" |
| size="small" |
| icon={<PlayCircleOutlined />} |
| onClick={handleRun} |
| style={{ background: '#10b981', borderColor: '#10b981' }} |
| > |
| 运行 |
| </Button> |
| ) : ( |
| <Button |
| danger |
| size="small" |
| icon={<StopOutlined />} |
| onClick={handleStop} |
| > |
| 停止 |
| </Button> |
| )} |
| <Button |
| size="small" |
| icon={<ClearOutlined />} |
| onClick={handleClear} |
| > |
| 清除 |
| </Button> |
| </Space> |
| </div> |
| |
| {/* 代码编辑器 */} |
| <div style={{ |
| flex: '0 0 auto', |
| maxHeight: '45%', |
| overflow: 'auto', |
| borderRadius: '6px', |
| border: '1px solid rgba(0,0,0,0.1)' |
| }}> |
| <CodeMirror |
| value={code} |
| height="auto" |
| minHeight="120px" |
| maxHeight="300px" |
| extensions={[python()]} |
| onChange={(val) => setCode(val)} |
| theme="light" |
| basicSetup={{ |
| lineNumbers: true, |
| highlightActiveLine: true, |
| foldGutter: true, |
| }} |
| /> |
| </div> |
| |
| {/* 终端输出 */} |
| <div style={{ |
| flex: 1, |
| minHeight: '120px', |
| background: '#1e1e1e', |
| borderRadius: '6px', |
| padding: '12px', |
| overflow: 'auto', |
| fontFamily: "'JetBrains Mono', 'Fira Code', monospace", |
| fontSize: '13px', |
| }}> |
| {output.map((line, i) => ( |
| <div key={i} style={{ color: getOutputColor(line.type), lineHeight: '1.6', whiteSpace: 'pre-wrap' }}> |
| {line.text} |
| </div> |
| ))} |
| {needsInput && ( |
| <div style={{ display: 'flex', gap: '8px', marginTop: '8px', alignItems: 'center' }}> |
| <span style={{ color: '#10b981', flexShrink: 0 }}>{'>>>'}</span> |
| <Input |
| size="small" |
| value={userInput} |
| onChange={e => setUserInput(e.target.value)} |
| onPressEnter={handleInput} |
| placeholder="输入内容后按回车..." |
| style={{ |
| flex: 1, |
| background: '#2d2d2d', |
| border: '1px solid #444', |
| color: '#e5e7eb', |
| fontFamily: 'monospace' |
| }} |
| autoFocus |
| /> |
| </div> |
| )} |
| <div ref={outputEndRef} /> |
| </div> |
| </div> |
| ); |
| }; |
|
|
| export default CodeExecutor; |
|
|