feat: Linux练习平台

- Web界面Linux命令练习
- Python后端 + sandbox安全沙箱
- 课程和任务管理
This commit is contained in:
likingcode
2026-03-07 05:43:51 +00:00
commit 5686831d9a
22 changed files with 8816 additions and 0 deletions

926
index_new.html Normal file
View File

@@ -0,0 +1,926 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Linux 命令学习平台 - 从入门到精通</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
:root {
--primary: #4CAF50;
--primary-dark: #388E3C;
--secondary: #2196F3;
--accent: #FF9800;
--bg: #f5f7fa;
--card-bg: #ffffff;
--text: #333333;
--text-light: #666666;
--border: #e0e0e0;
--success: #4CAF50;
--error: #f44336;
--warning: #ff9800;
}
[data-theme="dark"] {
--bg: #1a1a2e;
--card-bg: #16213e;
--text: #eaeaea;
--text-light: #a0a0a0;
--border: #0f3460;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Microsoft YaHei', sans-serif;
background: var(--bg);
color: var(--text);
line-height: 1.6;
}
/* Header */
.header {
background: linear-gradient(135deg, var(--primary) 0%, var(--primary-dark) 100%);
color: white;
padding: 1rem 2rem;
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
}
.header-content {
max-width: 1400px;
margin: 0 auto;
display: flex;
justify-content: space-between;
align-items: center;
}
.logo {
font-size: 1.5rem;
font-weight: bold;
}
.header-actions {
display: flex;
gap: 1rem;
align-items: center;
}
.mode-toggle {
background: rgba(255,255,255,0.2);
border: none;
color: white;
padding: 0.5rem 1rem;
border-radius: 20px;
cursor: pointer;
transition: all 0.3s;
}
.mode-toggle:hover {
background: rgba(255,255,255,0.3);
}
.mode-toggle.active {
background: white;
color: var(--primary);
}
/* Main Layout */
.container {
max-width: 1400px;
margin: 0 auto;
display: grid;
grid-template-columns: 280px 1fr 350px;
gap: 1.5rem;
padding: 1.5rem;
min-height: calc(100vh - 70px);
}
/* Sidebar */
.sidebar {
background: var(--card-bg);
border-radius: 12px;
padding: 1rem;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
height: fit-content;
}
.sidebar-title {
font-size: 1.1rem;
font-weight: 600;
margin-bottom: 1rem;
padding-bottom: 0.5rem;
border-bottom: 2px solid var(--primary);
}
.level-section {
margin-bottom: 1rem;
}
.level-header {
font-weight: 600;
color: var(--primary);
padding: 0.5rem;
background: rgba(76,175,80,0.1);
border-radius: 6px;
margin-bottom: 0.5rem;
cursor: pointer;
}
.task-list {
list-style: none;
}
.task-item {
padding: 0.6rem 0.8rem;
margin: 0.3rem 0;
border-radius: 6px;
cursor: pointer;
display: flex;
align-items: center;
gap: 0.5rem;
transition: all 0.2s;
font-size: 0.9rem;
}
.task-item:hover {
background: var(--bg);
}
.task-item.active {
background: var(--primary);
color: white;
}
.task-item.completed {
opacity: 0.7;
}
.task-status {
font-size: 1rem;
}
/* Main Content */
.main-content {
background: var(--card-bg);
border-radius: 12px;
padding: 2rem;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
}
.welcome-panel {
text-align: center;
padding: 3rem 2rem;
}
.welcome-title {
font-size: 2rem;
margin-bottom: 1rem;
background: linear-gradient(135deg, var(--primary), var(--secondary));
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
}
.welcome-desc {
color: var(--text-light);
font-size: 1.1rem;
margin-bottom: 2rem;
}
.start-btn {
background: linear-gradient(135deg, var(--primary) 0%, var(--primary-dark) 100%);
color: white;
border: none;
padding: 1rem 3rem;
font-size: 1.1rem;
border-radius: 30px;
cursor: pointer;
transition: transform 0.2s, box-shadow 0.2s;
}
.start-btn:hover {
transform: translateY(-2px);
box-shadow: 0 5px 20px rgba(76,175,80,0.4);
}
/* Task Panel */
.task-panel {
display: none;
}
.task-header {
margin-bottom: 1.5rem;
}
.task-title {
font-size: 1.5rem;
margin-bottom: 0.5rem;
}
.task-meta {
display: flex;
gap: 1rem;
color: var(--text-light);
font-size: 0.9rem;
}
.task-description {
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
padding: 1.5rem;
border-radius: 10px;
margin-bottom: 1.5rem;
border-left: 4px solid var(--primary);
}
.task-description h3 {
color: var(--primary);
margin-bottom: 0.8rem;
}
.task-description code {
background: rgba(76,175,80,0.1);
padding: 0.2rem 0.5rem;
border-radius: 4px;
color: var(--primary-dark);
font-family: 'Consolas', monospace;
}
/* Command Input Area */
.command-area {
background: #1e1e1e;
border-radius: 10px;
padding: 1rem;
margin-bottom: 1rem;
}
.command-input-wrapper {
display: flex;
align-items: center;
gap: 0.5rem;
margin-bottom: 0.5rem;
}
.prompt {
color: #4CAF50;
font-family: 'Consolas', monospace;
font-weight: bold;
}
.command-input {
flex: 1;
background: transparent;
border: none;
color: #fff;
font-family: 'Consolas', monospace;
font-size: 1rem;
outline: none;
}
.command-output {
background: #2d2d2d;
padding: 1rem;
border-radius: 6px;
min-height: 100px;
max-height: 300px;
overflow-y: auto;
font-family: 'Consolas', monospace;
color: #ddd;
white-space: pre-wrap;
margin-top: 0.5rem;
}
.action-buttons {
display: flex;
gap: 1rem;
margin-top: 1rem;
}
.btn {
padding: 0.8rem 1.5rem;
border: none;
border-radius: 6px;
cursor: pointer;
font-size: 1rem;
transition: all 0.2s;
}
.btn-primary {
background: var(--primary);
color: white;
}
.btn-primary:hover {
background: var(--primary-dark);
}
.btn-secondary {
background: var(--border);
color: var(--text);
}
.btn-hint {
background: var(--accent);
color: white;
}
/* Learning Panel (Right Sidebar) */
.learning-panel {
background: var(--card-bg);
border-radius: 12px;
padding: 1.5rem;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
height: fit-content;
}
.panel-title {
font-size: 1.1rem;
font-weight: 600;
margin-bottom: 1rem;
display: flex;
align-items: center;
gap: 0.5rem;
}
.panel-title::before {
content: "📚";
}
.command-detail {
display: none;
}
.command-detail.active {
display: block;
}
.cmd-name {
font-size: 1.3rem;
font-weight: bold;
color: var(--primary);
margin-bottom: 0.5rem;
font-family: 'Consolas', monospace;
}
.cmd-desc {
color: var(--text-light);
margin-bottom: 1rem;
line-height: 1.8;
}
.cmd-section {
margin-bottom: 1.2rem;
}
.cmd-section-title {
font-weight: 600;
color: var(--secondary);
margin-bottom: 0.5rem;
font-size: 0.95rem;
}
.param-list {
list-style: none;
}
.param-item {
padding: 0.5rem;
background: var(--bg);
border-radius: 6px;
margin-bottom: 0.5rem;
font-size: 0.9rem;
}
.param-name {
font-family: 'Consolas', monospace;
color: var(--accent);
font-weight: 600;
}
.example-box {
background: #f8f9fa;
border-left: 3px solid var(--secondary);
padding: 0.8rem;
border-radius: 0 6px 6px 0;
margin: 0.5rem 0;
}
.example-box code {
font-family: 'Consolas', monospace;
color: var(--primary-dark);
}
/* Success Panel */
.success-panel {
display: none;
background: linear-gradient(135deg, #d4edda 0%, #c3e6cb 100%);
border: 1px solid var(--success);
border-radius: 10px;
padding: 1.5rem;
margin-top: 1rem;
}
.success-panel.show {
display: block;
}
.success-title {
color: var(--success);
font-size: 1.2rem;
font-weight: 600;
margin-bottom: 0.5rem;
}
.deep-learning-btn {
background: var(--secondary);
color: white;
border: none;
padding: 0.8rem 1.5rem;
border-radius: 6px;
cursor: pointer;
margin-top: 1rem;
}
/* Progress Bar */
.progress-section {
margin-bottom: 1.5rem;
}
.progress-bar {
height: 8px;
background: var(--border);
border-radius: 4px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, var(--primary), var(--secondary));
transition: width 0.3s;
}
.progress-text {
text-align: center;
margin-top: 0.5rem;
color: var(--text-light);
font-size: 0.9rem;
}
/* Responsive */
@media (max-width: 1200px) {
.container {
grid-template-columns: 260px 1fr;
}
.learning-panel {
display: none;
}
}
@media (max-width: 768px) {
.container {
grid-template-columns: 1fr;
}
.sidebar {
display: none;
}
}
</style>
</head>
<body>
<header class="header">
<div class="header-content">
<div class="logo">🐧 Linux 学习平台</div>
<div class="header-actions">
<button class="mode-toggle active" id="learnMode" onclick="setMode('learn')">📖 学习模式</button>
<button class="mode-toggle" id="practiceMode" onclick="setMode('practice')">✏️ 练习模式</button>
<button class="mode-toggle" onclick="toggleTheme()">🌓</button>
</div>
</div>
</header>
<div class="container">
<!-- Left Sidebar: Course Navigation -->
<aside class="sidebar">
<div class="sidebar-title">📚 课程目录</div>
<div class="progress-section">
<div class="progress-bar">
<div class="progress-fill" id="progressFill" style="width: 0%"></div>
</div>
<div class="progress-text" id="progressText">0 / 95 完成</div>
</div>
<div id="courseNav"></div>
</aside>
<!-- Main Content: Task Area -->
<main class="main-content">
<!-- Welcome Panel -->
<div class="welcome-panel" id="welcomePanel">
<h1 class="welcome-title">🚀 开启 Linux 学习之旅</h1>
<p class="welcome-desc">
从入门到精通,系统学习 Linux 运维技能<br>
<strong>12 个级别 · 95 道题目 · 循序渐进</strong>
</p>
<button class="start-btn" onclick="startLearning()">开始学习</button>
</div>
<!-- Task Panel -->
<div class="task-panel" id="taskPanel">
<div class="task-header">
<h2 class="task-title" id="taskTitle">任务标题</h2>
<div class="task-meta">
<span id="taskLevel">Level 1</span>
<span>·</span>
<span id="taskNumber">1 / 95</span>
</div>
</div>
<div class="task-description" id="taskDescription">
<!-- 任务描述 -->
</div>
<!-- Command Input -->
<div class="command-area">
<div class="command-input-wrapper">
<span class="prompt">$</span>
<input type="text" class="command-input" id="cmdInput"
placeholder="输入 Linux 命令..."
onkeypress="if(event.key==='Enter')executeCommand()">
</div>
<div class="command-output" id="cmdOutput">输出将显示在这里...</div>
</div>
<div class="action-buttons">
<button class="btn btn-primary" onclick="executeCommand()">▶ 执行命令</button>
<button class="btn btn-hint" onclick="showHint()">💡 提示</button>
<button class="btn btn-secondary" onclick="showAnswer()">👀 查看答案</button>
</div>
<!-- Success Panel -->
<div class="success-panel" id="successPanel">
<div class="success-title">🎉 回答正确!</div>
<p id="successMessage">恭喜你完成了这个任务!</p>
<button class="deep-learning-btn" onclick="showDeepLearning()">
📚 查看深入学习资料
</button>
<button class="btn btn-primary" onclick="nextTask()" style="margin-left: 1rem;">
下一题 →
</button>
</div>
</div>
</main>
<!-- Right Sidebar: Learning Panel -->
<aside class="learning-panel" id="learningPanel">
<div class="panel-title">命令详解</div>
<div id="commandDetail">
<p style="color: var(--text-light); text-align: center; padding: 2rem 0;">
选择一个任务开始学习<br>
每个命令都有详细讲解
</p>
</div>
</aside>
</div>
<script>
// ====================
// 全局状态
// ====================
let COURSE_DATA = null;
let currentMode = 'learn'; // 'learn' 或 'practice'
let currentTask = null;
let completedTasks = JSON.parse(localStorage.getItem('linux_completed') || '[]');
let currentTheme = localStorage.getItem('linux_theme') || 'light';
// ====================
// 命令知识库
// ====================
const COMMAND_KNOWLEDGE = {
'pwd': {
name: 'pwd',
fullName: 'Print Working Directory',
description: '显示当前工作目录的完整路径。这是你在文件系统中的"当前位置"。',
commonParams: [
{ param: '-L', desc: '显示逻辑路径(包括符号链接)' },
{ param: '-P', desc: '显示物理路径(解析所有符号链接)' }
],
examples: [
{ cmd: 'pwd', desc: '显示当前目录' },
{ cmd: 'pwd -P', desc: '显示物理路径' }
],
tips: '当你不确定自己在哪个目录时,随时使用 pwd 确认位置。',
related: ['cd', 'ls']
},
'ls': {
name: 'ls',
fullName: 'List Directory Contents',
description: '列出目录中的文件和子目录。这是最常用的文件查看命令。',
commonParams: [
{ param: '-l', desc: '长格式显示,包含权限、所有者、大小等详细信息' },
{ param: '-a', desc: '显示所有文件,包括以点开头的隐藏文件' },
{ param: '-h', desc: '人类可读格式,文件大小显示为 K、M、G' },
{ param: '-t', desc: '按修改时间排序' },
{ param: '-r', desc: '反向排序' },
{ param: '-S', desc: '按文件大小排序' }
],
examples: [
{ cmd: 'ls', desc: '列出当前目录文件' },
{ cmd: 'ls -la', desc: '详细列出所有文件' },
{ cmd: 'ls -lh', desc: '详细列出,人类可读大小' },
{ cmd: 'ls -lt', desc: '按时间排序列出' }
],
tips: 'ls -la 是查看目录内容最常用的组合命令。',
related: ['pwd', 'cd', 'll']
},
'cd': {
name: 'cd',
fullName: 'Change Directory',
description: '切换当前工作目录。这是文件系统导航的基础命令。',
commonParams: [
{ param: '..', desc: '切换到上级目录' },
{ param: '~', desc: '切换到用户主目录' },
{ param: '-', desc: '切换到刚才所在的目录' }
],
examples: [
{ cmd: 'cd /home', desc: '切换到 /home 目录' },
{ cmd: 'cd ..', desc: '切换到上级目录' },
{ cmd: 'cd ~', desc: '切换到主目录' },
{ cmd: 'cd -', desc: '返回刚才的目录' }
],
tips: 'cd - 可以快速在两个目录之间切换。',
related: ['pwd', 'ls']
},
'cat': {
name: 'cat',
fullName: 'Concatenate',
description: '连接文件并输出内容。常用于查看小文件的内容。',
commonParams: [
{ param: '-n', desc: '显示行号' },
{ param: '-b', desc: '显示行号,但空行不编号' },
{ param: '-s', desc: '压缩多个空行为一个' }
],
examples: [
{ cmd: 'cat file.txt', desc: '显示文件内容' },
{ cmd: 'cat -n file.txt', desc: '显示文件内容并带行号' },
{ cmd: 'cat file1 file2', desc: '连接多个文件' }
],
tips: '对于大文件,建议使用 less 或 more 而不是 cat。',
related: ['less', 'more', 'head', 'tail']
}
};
// ====================
// 初始化
// ====================
document.addEventListener('DOMContentLoaded', async () => {
applyTheme(currentTheme);
await loadCourseData();
renderCourseNav();
updateProgress();
});
async function loadCourseData() {
try {
const res = await fetch('/api/tasks');
if (res.ok) {
COURSE_DATA = await res.json();
console.log('✅ 课程数据加载成功');
}
} catch (e) {
console.error('❌ 加载课程数据失败:', e);
}
}
// ====================
// 模式切换
// ====================
function setMode(mode) {
currentMode = mode;
document.getElementById('learnMode').classList.toggle('active', mode === 'learn');
document.getElementById('practiceMode').classList.toggle('active', mode === 'practice');
if (currentTask) {
renderTask(currentTask);
}
}
function toggleTheme() {
currentTheme = currentTheme === 'light' ? 'dark' : 'light';
applyTheme(currentTheme);
localStorage.setItem('linux_theme', currentTheme);
}
function applyTheme(theme) {
document.documentElement.setAttribute('data-theme', theme);
}
// ====================
// 课程导航
// ====================
function renderCourseNav() {
if (!COURSE_DATA) return;
const nav = document.getElementById('courseNav');
let html = '';
COURSE_DATA.levels.forEach((level, levelIdx) => {
html += `
<div class="level-section">
<div class="level-header" onclick="toggleLevel(${levelIdx})">
${level.title}
</div>
<ul class="task-list" id="level-${levelIdx}">
`;
level.challenges.forEach((task, taskIdx) => {
const isCompleted = completedTasks.includes(task.id);
const isActive = currentTask && currentTask.id === task.id;
html += `
<li class="task-item ${isActive ? 'active' : ''} ${isCompleted ? 'completed' : ''}"
onclick="selectTask('${level.id}', '${task.id}')">
<span class="task-status">${isCompleted ? '✅' : '○'}</span>
<span>${taskIdx + 1}. ${task.title}</span>
</li>
`;
});
html += '</ul></div>';
});
nav.innerHTML = html;
}
function toggleLevel(levelIdx) {
const list = document.getElementById(`level-${levelIdx}`);
list.style.display = list.style.display === 'none' ? 'block' : 'none';
}
function selectTask(levelId, taskId) {
if (!COURSE_DATA) return;
const level = COURSE_DATA.levels.find(l => l.id === levelId);
const task = level.challenges.find(t => t.id === taskId);
currentTask = { ...task, level: level.title, levelNum: COURSE_DATA.levels.indexOf(level) + 1 };
renderTask(currentTask);
}
// ====================
// 任务渲染
// ====================
function renderTask(task) {
document.getElementById('welcomePanel').style.display = 'none';
document.getElementById('taskPanel').style.display = 'block';
document.getElementById('successPanel').classList.remove('show');
// 基本信息
document.getElementById('taskTitle').textContent = task.title;
document.getElementById('taskLevel').textContent = task.level;
document.getElementById('taskNumber').textContent = `${task.levelNum} / 95`;
// 任务描述
let descHtml = `<h3>📝 任务描述</h3><p>${task.description}</p>`;
// 学习模式:显示更多学习资料
if (currentMode === 'learn') {
descHtml += `
<div style="margin-top: 1rem; padding: 1rem; background: rgba(33,150,243,0.1); border-radius: 8px;">
<strong>💡 学习提示:</strong> ${task.hint}
</div>
`;
}
document.getElementById('taskDescription').innerHTML = descHtml;
// 清空输出
document.getElementById('cmdOutput').textContent = '输出将显示在这里...';
document.getElementById('cmdInput').value = '';
// 更新学习面板
updateLearningPanel(task);
// 更新导航高亮
renderCourseNav();
}
function updateLearningPanel(task) {
const panel = document.getElementById('commandDetail');
// 从任务描述中提取命令
const cmdMatch = task.description.match(/<code>(\w+)<\/code>/);
const cmdName = cmdMatch ? cmdMatch[1] : null;
if (cmdName && COMMAND_KNOWLEDGE[cmdName]) {
const cmd = COMMAND_KNOWLEDGE[cmdName];
let html = `
<div class="command-detail active">
<div class="cmd-name">${cmd.name}</div>
<div style="color: var(--text-light); font-size: 0.9rem; margin-bottom: 1rem;">
${cmd.fullName}
</div>
<div class="cmd-desc">${cmd.description}</div>
<div class="cmd-section">
<div class="cmd-section-title">🔧 常用参数</div>
<ul class="param-list">
`;
cmd.commonParams.forEach(param => {
html += `
<li class="param-item">
<span class="param-name">${param.param}</span>
<span style="color: var(--text-light);"> - ${param.desc}</span>
</li>
`;
});
html += `</ul></div>
<div class="cmd-section">
<div class="cmd-section-title">📝 使用示例</div>
`;
cmd.examples.forEach(ex => {
html += `
<div class="example-box">
<code>${ex.cmd}</code>
<div style="color: var(--text-light); margin-top: 0.3rem; font-size: 0.85rem;">
${ex.desc}
</div>
</div>
`;
});
html += `
</div>
<div class="cmd-section">
<div class="cmd-section-title">💡 小贴士</div>
<p style="font-size: 0.9rem; color: var(--text-light);">${cmd.tips}</p>
</div>
</div>
`;
panel.innerHTML = html;
} else {
panel.innerHTML = `
<div class="command-detail active">
<p style="color: var(--text-light);">
本任务涉及多个命令组合使用<br>
完成任务后可查看详细解析
</p>
</div>
`;
}
}
// ====================
// 命令执行
// ====================
async function executeCommand() {
const cmd = document.getElementById('cmdInput').value.trim();
if (!cmd) return;
const outputEl = document.getElementById('cmdOutput');
outputEl.textContent = `执行: ${cmd}\n正在运行...`;
try {
const res = await fetch('/api/run?cmd=' + encodeURIComponent(cmd));
const data = await res.json();
outputEl.textContent = data.output || data.message || '(无输出)';
// 检查答案
checkAnswer(cmd, data.output || '');
} catch (e) {
outputEl.textContent = `❌ 错误: ${e.message}`;
}
}
function checkAnswer(cmd, output) {
if (!currentTask) return;
// 简化检查:命令是否包含在 solution 中
const isCorrect = currentTask.solution && currentTask.solution.some(s =>
cmd.toLowerCase().includes(s.toLowerCase())
);
if (isCorrect) {
showSuccess();
}
}
function showSuccess() {
if (!completedTasks.includes(currentTask.id)) {
completedTasks.push(currentTask.id);
localStorage.setItem('linux_completed', JSON.stringify(completedTasks));
updateProgress();
}
document.getElementById('successPanel').classList.add('show');
document.getElementById('successMessage').textContent =
currentTask.success_msg || '恭喜你完成了这个任务!';
renderCourseNav();
}
function showDeepLearning() {
// 显示深入学习资料
alert('📚 深入学习资料功能开发中...\n\n这里将显示\n- 命令原理解析\n- 实际应用场景\n- 常见错误分析\n- 相关命令对比');
}
// ====================
// 辅助功能
// ====================
function showHint() {
if (!currentTask) return;
alert('💡 提示:\n\n' + currentTask.hint);
}
function showAnswer() {
if (!currentTask) return;
const answer = currentTask.solution ? currentTask.solution[0] : '暂无答案';
if (confirm('查看答案将标记此题为"已学习",确定要继续吗?')) {
document.getElementById('cmdInput').value = answer;
showSuccess();
}
}
function nextTask() {
if (!COURSE_DATA || !currentTask) return;
// 找到当前任务的下一个
let found = false;
for (const level of COURSE_DATA.levels) {
for (const task of level.challenges) {
if (found) {
selectTask(level.id, task.id);
return;
}
if (task.id === currentTask.id) found = true;
}
}
alert('🎉 恭喜!你已完成所有课程!');
}
function startLearning() {
if (!COURSE_DATA) {
alert('课程数据加载中,请稍候...');
return;
}
selectTask(COURSE_DATA.levels[0].id, COURSE_DATA.levels[0].challenges[0].id);
}
function updateProgress() {
const total = 95;
const completed = completedTasks.length;
const percent = Math.round((completed / total) * 100);
document.getElementById('progressFill').style.width = percent + '%';
document.getElementById('progressText').textContent = `${completed} / ${total} 完成 (${percent}%)`;
}
// ====================
// 全局函数暴露
// ====================
window.setMode = setMode;
window.toggleTheme = toggleTheme;
window.toggleLevel = toggleLevel;
window.selectTask = selectTask;
window.executeCommand = executeCommand;
window.showHint = showHint;
window.showAnswer = showAnswer;
window.showDeepLearning = showDeepLearning;
window.nextTask = nextTask;
window.startLearning = startLearning;
</script>
</body>
</html>