Phase G2-1~G2-6: Code Execution Worker - Docker sandbox deployment, backend API (code_exec_admin.py), frontend CodeExecTab, security hardening (read-only FS, non-root, capability dropping, RLIMIT_AS)
This commit is contained in:
1 parent
909c013e2d
commit
9b3e15ace6
9 files changed
+1709
-447
No files matched your search
@@ -0,0 +1,747 @@
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { adminAPI } from '@/api';
|
||||
|
||||
interface ExecStats {
|
||||
total: number;
|
||||
today: number;
|
||||
success: number;
|
||||
failed: number;
|
||||
timeout: number;
|
||||
avg_execution_time_ms: number;
|
||||
by_language: Record<string, number>;
|
||||
}
|
||||
|
||||
interface RuntimeInfo {
|
||||
python?: string;
|
||||
nodejs?: string;
|
||||
max_code_length?: number;
|
||||
max_timeout?: number;
|
||||
}
|
||||
|
||||
interface TaskItem {
|
||||
id: string;
|
||||
language: string;
|
||||
code_preview: string;
|
||||
status: string;
|
||||
exit_code: number | null;
|
||||
execution_time_ms: number | null;
|
||||
error_msg: string | null;
|
||||
truncated: boolean;
|
||||
created_at: string;
|
||||
completed_at: string | null;
|
||||
}
|
||||
|
||||
interface ExecResult {
|
||||
task_id: string;
|
||||
success: boolean;
|
||||
stdout: string;
|
||||
stderr: string;
|
||||
exit_code: number;
|
||||
execution_time_ms: number;
|
||||
error: string | null;
|
||||
truncated: boolean;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
const LANG_CONFIG: Record<string, { label: string; color: string; bg: string; placeholder: string }> = {
|
||||
python: { label: 'Python', color: '#306998', bg: '#E8F4FD', placeholder: '# Python 代码示例\nprint("Hello, sandbox!")\n\nfor i in range(5):\n print(f"Line {i}")' },
|
||||
javascript: { label: 'JavaScript', color: '#F7DF1E', bg: '#FFFDE8', placeholder: '// JavaScript 代码示例\nconsole.log("Hello, sandbox!");\n\nfor (let i = 0; i < 5; i++) {\n console.log(`Line ${i}`);\n}' },
|
||||
};
|
||||
|
||||
const STATUS_CONFIG: Record<string, { label: string; color: string; bg: string }> = {
|
||||
completed: { label: '成功', color: '#10B981', bg: '#D1FAE5' },
|
||||
error: { label: '错误', color: '#EF4444', bg: '#FEE2E2' },
|
||||
failed: { label: '失败', color: '#F59E0B', bg: '#FEF3C7' },
|
||||
running: { label: '运行中', color: '#3B82F6', bg: '#DBEAFE' },
|
||||
};
|
||||
|
||||
const PAGE_SIZE = 15;
|
||||
|
||||
const CodeExec: React.FC = () => {
|
||||
// Stats
|
||||
const [stats, setStats] = useState<ExecStats | null>(null);
|
||||
const [runtimes, setRuntimes] = useState<RuntimeInfo | null>(null);
|
||||
|
||||
// Editor
|
||||
const [language, setLanguage] = useState('python');
|
||||
const [code, setCode] = useState(LANG_CONFIG.python.placeholder);
|
||||
const [timeout, setTimeout_] = useState(30);
|
||||
|
||||
// Execution
|
||||
const [executing, setExecuting] = useState(false);
|
||||
const [result, setResult] = useState<ExecResult | null>(null);
|
||||
const [activeTab, setActiveTab] = useState<'stdout' | 'stderr'>('stdout');
|
||||
|
||||
// Task History
|
||||
const [tasks, setTasks] = useState<TaskItem[]>([]);
|
||||
const [total, setTotal] = useState(0);
|
||||
const [page, setPage] = useState(1);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [filterLang, setFilterLang] = useState('');
|
||||
const [filterStatus, setFilterStatus] = useState('');
|
||||
|
||||
// Task Detail Modal
|
||||
const [detailTask, setDetailTask] = useState<TaskItem | null>(null);
|
||||
const [detailResult, setDetailResult] = useState<ExecResult | null>(null);
|
||||
const [detailLoading, setDetailLoading] = useState(false);
|
||||
|
||||
// Toast
|
||||
const [toast, setToast] = useState<{ msg: string; type: 'success' | 'error' } | null>(null);
|
||||
const showToast = (msg: string, type: 'success' | 'error' = 'success') => {
|
||||
setToast({ msg, type });
|
||||
setTimeout(() => setToast(null), 3000);
|
||||
};
|
||||
|
||||
const outputRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
// Load stats & runtimes on mount
|
||||
useEffect(() => {
|
||||
loadStats();
|
||||
loadRuntimes();
|
||||
}, []);
|
||||
|
||||
// Load tasks
|
||||
useEffect(() => {
|
||||
loadTasks();
|
||||
}, [page, filterLang, filterStatus]);
|
||||
|
||||
const loadStats = async () => {
|
||||
try {
|
||||
const res = await adminAPI.getCodeExecStats() as any;
|
||||
setStats(res.data || null);
|
||||
} catch (e) { /* ignore */ }
|
||||
};
|
||||
|
||||
const loadRuntimes = async () => {
|
||||
try {
|
||||
const res = await adminAPI.getCodeExecRuntimes() as any;
|
||||
setRuntimes(res.data || null);
|
||||
} catch (e) { /* ignore */ }
|
||||
};
|
||||
|
||||
const loadTasks = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const params: any = { page, page_size: PAGE_SIZE };
|
||||
if (filterLang) params.language = filterLang;
|
||||
if (filterStatus) params.status = filterStatus;
|
||||
const res = await adminAPI.getCodeExecTasks(params) as any;
|
||||
const data = res.data || res;
|
||||
setTasks(data.items || []);
|
||||
setTotal(data.total || 0);
|
||||
} catch (e: any) {
|
||||
showToast('加载任务列表失败: ' + (e.message || e), 'error');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Execute code
|
||||
const handleExecute = async () => {
|
||||
if (!code.trim()) {
|
||||
showToast('请输入代码', 'error');
|
||||
return;
|
||||
}
|
||||
setExecuting(true);
|
||||
setResult(null);
|
||||
setActiveTab('stdout');
|
||||
try {
|
||||
const res = await adminAPI.executeCode({ language, code: code.trim(), timeout }) as any;
|
||||
setResult(res.data || res);
|
||||
} catch (e: any) {
|
||||
setResult({
|
||||
task_id: '',
|
||||
success: false,
|
||||
stdout: '',
|
||||
stderr: '',
|
||||
exit_code: -1,
|
||||
execution_time_ms: 0,
|
||||
error: e.message || '执行失败',
|
||||
truncated: false,
|
||||
created_at: '',
|
||||
});
|
||||
showToast('执行失败: ' + (e.message || e), 'error');
|
||||
} finally {
|
||||
setExecuting(false);
|
||||
loadStats();
|
||||
loadTasks();
|
||||
}
|
||||
};
|
||||
|
||||
// View task detail
|
||||
const viewTaskDetail = async (task: TaskItem) => {
|
||||
setDetailTask(task);
|
||||
setDetailResult(null);
|
||||
setDetailLoading(true);
|
||||
try {
|
||||
const res = await adminAPI.getCodeExecTask(task.id) as any;
|
||||
setDetailResult(res.data || null);
|
||||
} catch (e: any) {
|
||||
showToast('获取任务详情失败', 'error');
|
||||
} finally {
|
||||
setDetailLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Delete task
|
||||
const handleDeleteTask = async (task: TaskItem) => {
|
||||
if (!window.confirm(`确定要删除任务 ${task.id} 吗?`)) return;
|
||||
try {
|
||||
await adminAPI.deleteCodeExecTask(task.id);
|
||||
showToast('删除成功');
|
||||
if (detailTask?.id === task.id) {
|
||||
setDetailTask(null);
|
||||
setDetailResult(null);
|
||||
}
|
||||
loadTasks();
|
||||
loadStats();
|
||||
} catch (e: any) {
|
||||
showToast('删除失败: ' + (e.message || e), 'error');
|
||||
}
|
||||
};
|
||||
|
||||
// Keyboard shortcut: Ctrl/Cmd+Enter to execute
|
||||
const handleKeyDown = (e: React.KeyboardEvent) => {
|
||||
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') {
|
||||
e.preventDefault();
|
||||
handleExecute();
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="admin-page">
|
||||
<div className="admin-page-header">
|
||||
<h2>代码执行沙箱</h2>
|
||||
<span style={{ fontSize: '13px', color: '#6B7280', marginLeft: '12px' }}>
|
||||
安全隔离的 Python / JavaScript 代码执行环境
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Stats Cards */}
|
||||
{stats && (
|
||||
<div className="stats-grid" style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fit, minmax(140px, 1fr))', gap: '12px', marginBottom: '20px' }}>
|
||||
<div className="stat-card">
|
||||
<div className="stat-value">{stats.today}</div>
|
||||
<div className="stat-label">今日执行</div>
|
||||
</div>
|
||||
<div className="stat-card">
|
||||
<div className="stat-value" style={{ color: '#10B981' }}>{stats.success}</div>
|
||||
<div className="stat-label">成功</div>
|
||||
</div>
|
||||
<div className="stat-card">
|
||||
<div className="stat-value" style={{ color: '#EF4444' }}>{stats.failed + stats.timeout}</div>
|
||||
<div className="stat-label">失败/超时</div>
|
||||
</div>
|
||||
<div className="stat-card">
|
||||
<div className="stat-value" style={{ color: '#6C63FF' }}>{stats.avg_execution_time_ms.toFixed(0)}ms</div>
|
||||
<div className="stat-label">平均耗时</div>
|
||||
</div>
|
||||
<div className="stat-card">
|
||||
<div className="stat-value">{stats.total}</div>
|
||||
<div className="stat-label">总执行次数</div>
|
||||
</div>
|
||||
{/* Language breakdown */}
|
||||
<div className="stat-card" style={{ gridColumn: 'span 2' }}>
|
||||
<div style={{ display: 'flex', gap: '12px', flexWrap: 'wrap', marginTop: '4px' }}>
|
||||
{Object.entries(stats.by_language || {}).map(([lang, count]) => (
|
||||
<span key={lang} style={{
|
||||
padding: '2px 12px', borderRadius: '12px', fontSize: '12px',
|
||||
color: (LANG_CONFIG[lang] || {}).color || '#666',
|
||||
background: (LANG_CONFIG[lang] || {}).bg || '#f0f0f0',
|
||||
}}>
|
||||
{(LANG_CONFIG[lang] || {}).label || lang}: {count}
|
||||
</span>
|
||||
))}
|
||||
{Object.keys(stats.by_language || {}).length === 0 && (
|
||||
<span style={{ fontSize: '12px', color: '#9CA3AF' }}>暂无数据</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="stat-label">语言分布</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Main grid: Editor + Output */}
|
||||
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '20px', marginBottom: '24px' }}>
|
||||
{/* Left: Code Editor */}
|
||||
<div style={{
|
||||
background: '#1E1E2E', borderRadius: '12px', border: '1px solid #313244',
|
||||
overflow: 'hidden', display: 'flex', flexDirection: 'column',
|
||||
}}>
|
||||
{/* Editor header */}
|
||||
<div style={{
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
||||
padding: '10px 16px', borderBottom: '1px solid #313244',
|
||||
}}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '12px' }}>
|
||||
<div style={{ display: 'flex', gap: '6px' }}>
|
||||
<div style={{ width: '10px', height: '10px', borderRadius: '50%', background: '#FF5F57' }} />
|
||||
<div style={{ width: '10px', height: '10px', borderRadius: '50%', background: '#FFBD2E' }} />
|
||||
<div style={{ width: '10px', height: '10px', borderRadius: '50%', background: '#27CA40' }} />
|
||||
</div>
|
||||
<select
|
||||
value={language}
|
||||
onChange={(e) => {
|
||||
setLanguage(e.target.value);
|
||||
setCode(LANG_CONFIG[e.target.value]?.placeholder || '');
|
||||
setResult(null);
|
||||
}}
|
||||
style={{
|
||||
padding: '4px 12px', borderRadius: '6px', border: '1px solid #45475A',
|
||||
background: '#313244', color: '#CDD6F4', fontSize: '13px', cursor: 'pointer',
|
||||
}}
|
||||
>
|
||||
<option value="python">Python</option>
|
||||
<option value="javascript">JavaScript</option>
|
||||
</select>
|
||||
<span style={{ fontSize: '12px', color: '#6C7086' }}>
|
||||
{runtimes && (language === 'python' ? runtimes.python : runtimes.nodejs)}
|
||||
</span>
|
||||
</div>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||
<span style={{ fontSize: '12px', color: '#6C7086' }}>超时:</span>
|
||||
<input
|
||||
type="range"
|
||||
min={1}
|
||||
max={120}
|
||||
value={timeout}
|
||||
onChange={(e) => setTimeout_(parseInt(e.target.value))}
|
||||
style={{ width: '80px', accentColor: '#6C63FF' }}
|
||||
/>
|
||||
<span style={{ fontSize: '12px', color: '#CDD6F4', minWidth: '28px' }}>{timeout}s</span>
|
||||
<button
|
||||
onClick={handleExecute}
|
||||
disabled={executing}
|
||||
style={{
|
||||
padding: '6px 16px', borderRadius: '6px', border: 'none',
|
||||
background: executing ? '#45475A' : '#6C63FF',
|
||||
color: '#fff', cursor: executing ? 'not-allowed' : 'pointer',
|
||||
fontSize: '13px', fontWeight: 600, whiteSpace: 'nowrap',
|
||||
}}
|
||||
>
|
||||
{executing ? '执行中...' : '▶ 运行'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Code textarea */}
|
||||
<textarea
|
||||
value={code}
|
||||
onChange={(e) => setCode(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
placeholder={LANG_CONFIG[language]?.placeholder || '输入代码...'}
|
||||
spellCheck={false}
|
||||
style={{
|
||||
flex: 1, minHeight: '320px', padding: '16px', border: 'none',
|
||||
background: '#1E1E2E', color: '#CDD6F4', fontFamily: "'Fira Code', 'Cascadia Code', 'JetBrains Mono', 'Consolas', monospace",
|
||||
fontSize: '13px', lineHeight: 1.6, resize: 'vertical',
|
||||
outline: 'none', tabSize: 4,
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Footer hint */}
|
||||
<div style={{
|
||||
padding: '6px 16px', borderTop: '1px solid #313244',
|
||||
fontSize: '11px', color: '#585B70',
|
||||
}}>
|
||||
Ctrl+Enter 运行 · {language === 'python' ? 'Python 3.11' : 'Node.js'} · 最大 {runtimes?.max_code_length ? (runtimes.max_code_length / 1024).toFixed(0) + 'KB' : '50KB'}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right: Output */}
|
||||
<div style={{
|
||||
background: '#fff', borderRadius: '12px', border: '1px solid #E5E7EB',
|
||||
overflow: 'hidden', display: 'flex', flexDirection: 'column',
|
||||
}}>
|
||||
{/* Output header */}
|
||||
<div style={{
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'space-between',
|
||||
padding: '10px 16px', borderBottom: '1px solid #E5E7EB',
|
||||
background: '#F9FAFB',
|
||||
}}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||
<span style={{ fontSize: '14px', fontWeight: 600, color: '#374151' }}>执行结果</span>
|
||||
{result && (
|
||||
<span style={{
|
||||
padding: '2px 8px', borderRadius: '4px', fontSize: '11px',
|
||||
color: result.success ? '#fff' : '#fff',
|
||||
background: result.success ? '#10B981' : (result.error ? '#EF4444' : '#F59E0B'),
|
||||
}}>
|
||||
{result.success ? '成功' : (result.error?.includes('timeout') ? '超时' : '失败')}
|
||||
</span>
|
||||
)}
|
||||
{result && !result.success && result.exit_code !== 0 && (
|
||||
<span style={{ fontSize: '12px', color: '#6B7280' }}>
|
||||
exit_code={result.exit_code}
|
||||
</span>
|
||||
)}
|
||||
{result && (
|
||||
<span style={{ fontSize: '12px', color: '#6B7280' }}>
|
||||
{result.execution_time_ms.toFixed(0)}ms
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{result && (
|
||||
<div style={{ display: 'flex', gap: '4px' }}>
|
||||
<button
|
||||
onClick={() => setActiveTab('stdout')}
|
||||
style={{
|
||||
padding: '4px 10px', borderRadius: '4px', border: 'none',
|
||||
background: activeTab === 'stdout' ? '#6C63FF' : '#F3F4F6',
|
||||
color: activeTab === 'stdout' ? '#fff' : '#6B7280',
|
||||
cursor: 'pointer', fontSize: '12px',
|
||||
}}
|
||||
>
|
||||
stdout
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('stderr')}
|
||||
style={{
|
||||
padding: '4px 10px', borderRadius: '4px', border: 'none',
|
||||
background: activeTab === 'stderr' ? '#EF4444' : '#F3F4F6',
|
||||
color: activeTab === 'stderr' ? '#fff' : '#6B7280',
|
||||
cursor: 'pointer', fontSize: '12px',
|
||||
}}
|
||||
>
|
||||
stderr{result.stderr ? ` (${result.stderr.length})` : ''}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Output content */}
|
||||
<div ref={outputRef} style={{
|
||||
flex: 1, minHeight: '320px', padding: '16px', overflow: 'auto',
|
||||
background: '#1E1E2E',
|
||||
}}>
|
||||
{executing ? (
|
||||
<div style={{ color: '#6C7086', fontSize: '13px', fontFamily: 'monospace' }}>
|
||||
<span style={{ animation: 'blink 1s infinite' }}>⏳</span> 正在执行...
|
||||
</div>
|
||||
) : !result ? (
|
||||
<div style={{ color: '#585B70', fontSize: '13px', fontFamily: 'monospace', textAlign: 'center', paddingTop: '60px' }}>
|
||||
<div style={{ fontSize: '36px', marginBottom: '12px' }}>{'</>'}</div>
|
||||
<div>点击「运行」按钮执行代码</div>
|
||||
<div style={{ marginTop: '8px', fontSize: '11px' }}>或按 Ctrl+Enter 快捷运行</div>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{result.error && (
|
||||
<div style={{
|
||||
padding: '8px 12px', marginBottom: '12px', borderRadius: '6px',
|
||||
background: '#FEE2E2', color: '#DC2626', fontSize: '12px',
|
||||
fontFamily: "'Fira Code', monospace",
|
||||
}}>
|
||||
{result.error}
|
||||
</div>
|
||||
)}
|
||||
{result.truncated && (
|
||||
<div style={{
|
||||
padding: '4px 12px', marginBottom: '8px', borderRadius: '4px',
|
||||
background: '#FEF3C7', color: '#92400E', fontSize: '11px', fontFamily: 'monospace',
|
||||
}}>
|
||||
输出已截断 (超过 500KB)
|
||||
</div>
|
||||
)}
|
||||
<pre style={{
|
||||
margin: 0, padding: 0, background: 'transparent', color: '#CDD6F4',
|
||||
fontSize: '13px', fontFamily: "'Fira Code', 'Cascadia Code', 'JetBrains Mono', 'Consolas', monospace",
|
||||
lineHeight: 1.6, whiteSpace: 'pre-wrap', wordBreak: 'break-all',
|
||||
minHeight: activeTab === 'stdout' && !result.stdout.trim() ? 'auto' : undefined,
|
||||
}}>
|
||||
{activeTab === 'stdout'
|
||||
? (result.stdout.trim() || <span style={{ color: '#585B70' }}>(无输出)</span>)
|
||||
: (result.stderr.trim() || <span style={{ color: '#585B70' }}>(无输出)</span>)
|
||||
}
|
||||
</pre>
|
||||
{result.task_id && (
|
||||
<div style={{ marginTop: '12px', padding: '6px 0', borderTop: '1px solid #313244', fontSize: '11px', color: '#585B70', fontFamily: 'monospace' }}>
|
||||
Task ID: {result.task_id}
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Task History */}
|
||||
<div style={{ marginTop: '8px' }}>
|
||||
<div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between', marginBottom: '12px' }}>
|
||||
<h3 style={{ margin: 0, fontSize: '16px', color: '#374151' }}>执行历史</h3>
|
||||
<div style={{ display: 'flex', gap: '8px' }}>
|
||||
<select
|
||||
value={filterLang}
|
||||
onChange={(e) => { setFilterLang(e.target.value); setPage(1); }}
|
||||
style={{ padding: '6px 10px', borderRadius: '6px', border: '1px solid #D1D5DB', fontSize: '13px' }}
|
||||
>
|
||||
<option value="">全部语言</option>
|
||||
<option value="python">Python</option>
|
||||
<option value="javascript">JavaScript</option>
|
||||
</select>
|
||||
<select
|
||||
value={filterStatus}
|
||||
onChange={(e) => { setFilterStatus(e.target.value); setPage(1); }}
|
||||
style={{ padding: '6px 10px', borderRadius: '6px', border: '1px solid #D1D5DB', fontSize: '13px' }}
|
||||
>
|
||||
<option value="">全部状态</option>
|
||||
<option value="completed">成功</option>
|
||||
<option value="error">错误</option>
|
||||
<option value="failed">失败</option>
|
||||
<option value="running">运行中</option>
|
||||
</select>
|
||||
<button
|
||||
onClick={loadTasks}
|
||||
style={{
|
||||
padding: '6px 12px', borderRadius: '6px', border: '1px solid #D1D5DB',
|
||||
background: '#fff', cursor: 'pointer', fontSize: '13px', color: '#374151',
|
||||
}}
|
||||
>
|
||||
刷新
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="table-container" style={{ background: '#fff', borderRadius: '12px', border: '1px solid #E5E7EB', overflow: 'hidden' }}>
|
||||
<table style={{ width: '100%', borderCollapse: 'collapse' }}>
|
||||
<thead>
|
||||
<tr style={{ background: '#F9FAFB', borderBottom: '1px solid #E5E7EB' }}>
|
||||
<th style={{ padding: '10px 16px', textAlign: 'left', fontSize: '13px', color: '#6B7280', fontWeight: 600, width: '100px' }}>任务ID</th>
|
||||
<th style={{ padding: '10px 16px', textAlign: 'left', fontSize: '13px', color: '#6B7280', fontWeight: 600, width: '80px' }}>语言</th>
|
||||
<th style={{ padding: '10px 16px', textAlign: 'left', fontSize: '13px', color: '#6B7280', fontWeight: 600 }}>代码预览</th>
|
||||
<th style={{ padding: '10px 16px', textAlign: 'center', fontSize: '13px', color: '#6B7280', fontWeight: 600, width: '80px' }}>状态</th>
|
||||
<th style={{ padding: '10px 16px', textAlign: 'center', fontSize: '13px', color: '#6B7280', fontWeight: 600, width: '80px' }}>耗时</th>
|
||||
<th style={{ padding: '10px 16px', textAlign: 'left', fontSize: '13px', color: '#6B7280', fontWeight: 600, width: '160px' }}>创建时间</th>
|
||||
<th style={{ padding: '10px 16px', textAlign: 'right', fontSize: '13px', color: '#6B7280', fontWeight: 600, width: '120px' }}>操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{loading ? (
|
||||
<tr><td colSpan={7} style={{ padding: '40px', textAlign: 'center', color: '#9CA3AF' }}>加载中...</td></tr>
|
||||
) : tasks.length === 0 ? (
|
||||
<tr><td colSpan={7} style={{ padding: '40px', textAlign: 'center', color: '#9CA3AF' }}>暂无执行记录</td></tr>
|
||||
) : (
|
||||
tasks.map(task => (
|
||||
<tr key={task.id} style={{ borderBottom: '1px solid #F3F4F6' }}>
|
||||
<td style={{ padding: '10px 16px', fontFamily: 'monospace', fontSize: '12px', color: '#6C63FF' }}>
|
||||
{task.id}
|
||||
</td>
|
||||
<td style={{ padding: '10px 16px' }}>
|
||||
<span style={{
|
||||
padding: '2px 8px', borderRadius: '12px', fontSize: '11px',
|
||||
color: (LANG_CONFIG[task.language] || {}).color || '#666',
|
||||
background: (LANG_CONFIG[task.language] || {}).bg || '#f0f0f0',
|
||||
}}>
|
||||
{(LANG_CONFIG[task.language] || {}).label || task.language}
|
||||
</span>
|
||||
</td>
|
||||
<td style={{ padding: '10px 16px', fontSize: '12px', color: '#6B7280', fontFamily: 'monospace', maxWidth: '280px', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
|
||||
{task.code_preview || '-'}
|
||||
</td>
|
||||
<td style={{ padding: '10px 16px', textAlign: 'center' }}>
|
||||
<span style={{
|
||||
padding: '2px 8px', borderRadius: '12px', fontSize: '11px',
|
||||
color: (STATUS_CONFIG[task.status] || {}).color || '#666',
|
||||
background: (STATUS_CONFIG[task.status] || {}).bg || '#f0f0f0',
|
||||
}}>
|
||||
{(STATUS_CONFIG[task.status] || {}).label || task.status}
|
||||
</span>
|
||||
</td>
|
||||
<td style={{ padding: '10px 16px', textAlign: 'center', fontSize: '12px', color: '#6B7280', fontFamily: 'monospace' }}>
|
||||
{task.execution_time_ms != null ? `${task.execution_time_ms}ms` : '-'}
|
||||
</td>
|
||||
<td style={{ padding: '10px 16px', fontSize: '12px', color: '#6B7280' }}>
|
||||
{task.created_at || '-'}
|
||||
</td>
|
||||
<td style={{ padding: '10px 16px', textAlign: 'right' }}>
|
||||
<div style={{ display: 'flex', gap: '4px', justifyContent: 'flex-end' }}>
|
||||
<button
|
||||
onClick={() => viewTaskDetail(task)}
|
||||
style={{
|
||||
padding: '4px 10px', borderRadius: '4px', border: '1px solid #D1D5DB',
|
||||
background: '#fff', cursor: 'pointer', fontSize: '11px', color: '#374151',
|
||||
}}
|
||||
>
|
||||
详情
|
||||
</button>
|
||||
<button
|
||||
onClick={() => handleDeleteTask(task)}
|
||||
style={{
|
||||
padding: '4px 10px', borderRadius: '4px', border: '1px solid #FEE2E2',
|
||||
background: '#FEF2F2', cursor: 'pointer', fontSize: '11px', color: '#EF4444',
|
||||
}}
|
||||
>
|
||||
删除
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
))
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|
||||
{/* Pagination */}
|
||||
{total > PAGE_SIZE && (
|
||||
<div style={{ display: 'flex', justifyContent: 'center', gap: '8px', marginTop: '16px' }}>
|
||||
<button
|
||||
disabled={page <= 1}
|
||||
onClick={() => setPage(p => p - 1)}
|
||||
style={{
|
||||
padding: '6px 16px', borderRadius: '6px', border: '1px solid #D1D5DB',
|
||||
background: '#fff', cursor: page <= 1 ? 'not-allowed' : 'pointer',
|
||||
opacity: page <= 1 ? 0.5 : 1,
|
||||
}}
|
||||
>
|
||||
上一页
|
||||
</button>
|
||||
<span style={{ padding: '6px 12px', color: '#6B7280', fontSize: '14px' }}>
|
||||
第 {page} / {Math.ceil(total / PAGE_SIZE)} 页 (共 {total} 条)
|
||||
</span>
|
||||
<button
|
||||
disabled={page >= Math.ceil(total / PAGE_SIZE)}
|
||||
onClick={() => setPage(p => p + 1)}
|
||||
style={{
|
||||
padding: '6px 16px', borderRadius: '6px', border: '1px solid #D1D5DB',
|
||||
background: '#fff', cursor: page >= Math.ceil(total / PAGE_SIZE) ? 'not-allowed' : 'pointer',
|
||||
opacity: page >= Math.ceil(total / PAGE_SIZE) ? 0.5 : 1,
|
||||
}}
|
||||
>
|
||||
下一页
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Task Detail Modal */}
|
||||
{detailTask && (
|
||||
<div className="modal-overlay" style={{
|
||||
position: 'fixed', top: 0, left: 0, right: 0, bottom: 0,
|
||||
background: 'rgba(0,0,0,0.5)', display: 'flex', alignItems: 'center',
|
||||
justifyContent: 'center', zIndex: 1000,
|
||||
}} onClick={() => { setDetailTask(null); setDetailResult(null); }}>
|
||||
<div className="modal-content" style={{
|
||||
background: '#fff', borderRadius: '12px', padding: '24px',
|
||||
width: '700px', maxHeight: '85vh', overflow: 'auto',
|
||||
boxShadow: '0 20px 60px rgba(0,0,0,0.2)',
|
||||
}} onClick={e => e.stopPropagation()}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: '16px' }}>
|
||||
<h3 style={{ margin: 0, fontSize: '16px' }}>任务详情 - {detailTask.id}</h3>
|
||||
<button
|
||||
onClick={() => { setDetailTask(null); setDetailResult(null); }}
|
||||
style={{ padding: '4px 8px', borderRadius: '4px', border: 'none', background: '#F3F4F6', cursor: 'pointer', fontSize: '16px', color: '#6B7280' }}
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Task info bar */}
|
||||
<div style={{ display: 'flex', gap: '12px', marginBottom: '16px', flexWrap: 'wrap' }}>
|
||||
<span style={{
|
||||
padding: '2px 10px', borderRadius: '12px', fontSize: '12px',
|
||||
color: (LANG_CONFIG[detailTask.language] || {}).color || '#666',
|
||||
background: (LANG_CONFIG[detailTask.language] || {}).bg || '#f0f0f0',
|
||||
}}>
|
||||
{(LANG_CONFIG[detailTask.language] || {}).label || detailTask.language}
|
||||
</span>
|
||||
<span style={{
|
||||
padding: '2px 10px', borderRadius: '12px', fontSize: '12px',
|
||||
color: (STATUS_CONFIG[detailTask.status] || {}).color || '#666',
|
||||
background: (STATUS_CONFIG[detailTask.status] || {}).bg || '#f0f0f0',
|
||||
}}>
|
||||
{(STATUS_CONFIG[detailTask.status] || {}).label || detailTask.status}
|
||||
</span>
|
||||
{detailTask.execution_time_ms != null && (
|
||||
<span style={{ fontSize: '12px', color: '#6B7280', fontFamily: 'monospace' }}>
|
||||
耗时: {detailTask.execution_time_ms}ms
|
||||
</span>
|
||||
)}
|
||||
{detailTask.exit_code != null && (
|
||||
<span style={{ fontSize: '12px', color: '#6B7280', fontFamily: 'monospace' }}>
|
||||
exit_code: {detailTask.exit_code}
|
||||
</span>
|
||||
)}
|
||||
<span style={{ fontSize: '12px', color: '#9CA3AF' }}>
|
||||
{detailTask.created_at}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* Code preview */}
|
||||
<div style={{ marginBottom: '16px' }}>
|
||||
<label style={{ display: 'block', marginBottom: '6px', fontSize: '13px', color: '#374151', fontWeight: 500 }}>代码预览</label>
|
||||
<pre style={{
|
||||
margin: 0, padding: '12px', borderRadius: '6px',
|
||||
background: '#1E1E2E', color: '#CDD6F4', fontSize: '12px',
|
||||
fontFamily: 'monospace', lineHeight: 1.5, maxHeight: '150px', overflow: 'auto',
|
||||
}}>
|
||||
{detailTask.code_preview || '(无)'}
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
{/* Full output */}
|
||||
{detailLoading ? (
|
||||
<div style={{ textAlign: 'center', padding: '40px', color: '#9CA3AF' }}>加载中...</div>
|
||||
) : detailResult ? (
|
||||
<>
|
||||
{detailResult.error && (
|
||||
<div style={{
|
||||
padding: '8px 12px', marginBottom: '12px', borderRadius: '6px',
|
||||
background: '#FEE2E2', color: '#DC2626', fontSize: '12px', fontFamily: 'monospace',
|
||||
}}>
|
||||
{detailResult.error}
|
||||
</div>
|
||||
)}
|
||||
{detailResult.stdout && (
|
||||
<div style={{ marginBottom: '12px' }}>
|
||||
<label style={{ display: 'block', marginBottom: '6px', fontSize: '13px', color: '#374151', fontWeight: 500 }}>stdout</label>
|
||||
<pre style={{
|
||||
margin: 0, padding: '12px', borderRadius: '6px',
|
||||
background: '#1E1E2E', color: '#CDD6F4', fontSize: '12px',
|
||||
fontFamily: 'monospace', lineHeight: 1.5, maxHeight: '300px', overflow: 'auto',
|
||||
whiteSpace: 'pre-wrap', wordBreak: 'break-all',
|
||||
}}>
|
||||
{detailResult.stdout}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
{detailResult.stderr && (
|
||||
<div style={{ marginBottom: '12px' }}>
|
||||
<label style={{ display: 'block', marginBottom: '6px', fontSize: '13px', color: '#EF4444', fontWeight: 500 }}>stderr</label>
|
||||
<pre style={{
|
||||
margin: 0, padding: '12px', borderRadius: '6px',
|
||||
background: '#1E1E2E', color: '#F38BA8', fontSize: '12px',
|
||||
fontFamily: 'monospace', lineHeight: 1.5, maxHeight: '300px', overflow: 'auto',
|
||||
whiteSpace: 'pre-wrap', wordBreak: 'break-all',
|
||||
}}>
|
||||
{detailResult.stderr}
|
||||
</pre>
|
||||
</div>
|
||||
)}
|
||||
{!detailResult.stdout.trim() && !detailResult.stderr.trim() && !detailResult.error && (
|
||||
<div style={{ textAlign: 'center', padding: '20px', color: '#9CA3AF', fontSize: '13px' }}>(无输出)</div>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<div style={{ textAlign: 'center', padding: '20px', color: '#9CA3AF', fontSize: '13px' }}>无法获取详细信息</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Toast */}
|
||||
{toast && (
|
||||
<div style={{
|
||||
position: 'fixed', top: '20px', right: '20px', zIndex: 2000,
|
||||
padding: '12px 24px', borderRadius: '8px', color: '#fff',
|
||||
background: toast.type === 'success' ? '#10B981' : '#EF4444',
|
||||
boxShadow: '0 4px 12px rgba(0,0,0,0.15)', fontSize: '14px',
|
||||
}}>
|
||||
{toast.msg}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default CodeExec;
|
||||
Reference in new issue
Block a user