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

648
index.html.debug Normal file
View File

@@ -0,0 +1,648 @@
<!DOCTYPE html>
<html>
<head>
<title>Linux 命令沙盒练习 - 菜鸟式学习平台</title>
<meta charset="utf-8">
<style>
* { box-sizing: border-box; }
:root {
--bg-color: #f5f5f5;
--text-color: #333;
--sidebar-bg: #fff;
--sidebar-border: #ddd;
--main-bg: #fff;
--cmd-bg: #f0f0f0;
--cmd-border: #ccc;
--cmd-text: #333;
--success-color: #4caf50;
--error-color: #f44336;
--highlight-color: #ff9800;
}
[data-theme="dark"] {
--bg-color: #1e1e1e;
--text-color: #e0e0e0;
--sidebar-bg: #2d2d2d;
--sidebar-border: #444;
--main-bg: #1e1e1e;
--cmd-bg: #2d2d2d;
--cmd-border: #444;
--cmd-text: #fff;
}
body {
font-family: 'Segoe UI', 'Microsoft YaHei', sans-serif;
background: var(--bg-color);
color: var(--text-color);
margin: 0;
padding: 0;
transition: background 0.3s, color 0.3s;
}
/* Header */
header {
background: var(--sidebar-bg);
border-bottom: 1px solid var(--sidebar-border);
padding: 10px 20px;
display: flex;
justify-content: space-between;
align-items: center;
}
header h1 { margin: 0; font-size: 20px; color: var(--success-color); }
.header-actions { display: flex; gap: 10px; align-items: center; }
.theme-toggle, .logout-btn {
background: transparent;
border: 1px solid var(--sidebar-border);
padding: 5px 10px;
border-radius: 4px;
cursor: pointer;
color: var(--text-color);
}
.theme-toggle:hover, .logout-btn:hover { background: var(--cmd-bg); }
/* Layout */
.container {
display: flex;
height: calc(100vh - 50px);
}
/* Sidebar */
.sidebar {
width: 260px;
background: var(--sidebar-bg);
border-right: 1px solid var(--sidebar-border);
overflow-y: auto;
padding: 15px;
}
.sidebar h2 {
font-size: 16px;
margin: 10px 0 5px;
color: var(--success-color);
border-bottom: 2px solid var(--highlight-color);
padding-bottom: 5px;
}
.lesson-list {
list-style: none;
padding: 0;
margin: 0;
}
.lesson-item {
padding: 8px 12px;
margin: 4px 0;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s;
display: flex;
justify-content: space-between;
align-items: center;
}
.lesson-item:hover { background: var(--cmd-bg); }
.lesson-item.active {
background: var(--highlight-color);
color: #fff;
}
.lesson-item.locked { opacity: 0.6; cursor: not-allowed; }
.lesson-badge {
font-size: 12px;
padding: 2px 6px;
border-radius: 10px;
background: #ccc;
color: #fff;
}
.lesson-item.active .lesson-badge { background: #fff; color: var(--highlight-color); }
.lesson-item.locked .lesson-badge { background: var(--highlight-color); }
/* Main Content */
.main-content {
flex: 1;
padding: 20px;
overflow-y: auto;
}
.task-card {
background: var(--main-bg);
border: 1px solid var(--sidebar-border);
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
}
.task-title {
font-size: 22px;
color: var(--success-color);
margin: 0 0 10px;
}
.task-desc {
background: var(--cmd-bg);
padding: 15px;
border-radius: 6px;
border-left: 4px solid var(--highlight-color);
margin: 10px 0;
line-height: 1.6;
}
.task-hint {
color: var(--highlight-color);
font-weight: bold;
margin: 10px 0;
}
.task-success { display: none; background: #e8f5e9; padding: 15px; border-radius: 6px; margin: 15px 0; border:1px solid var(--success-color); }
.task-success.show { display: block; }
.task-success h4 { margin: 0 0 5px; color: var(--success-color); }
/* Command Area */
.cmd-area {
margin-top: 20px;
}
.cmd-bar {
background: var(--cmd-bg);
border: 1px solid var(--cmd-border);
border-radius: 6px;
padding: 10px;
display: flex;
align-items: center;
}
.prompt { margin-right: 10px; color: var(--success-color); font-weight: bold; }
#cmd {
flex: 1;
background: transparent;
border: none;
color: var(--cmd-text);
font-family: 'Courier New', monospace;
font-size: 16px;
padding: 8px;
outline: none;
}
#output {
margin-top: 15px;
background: var(--sidebar-bg);
border: 1px solid var(--sidebar-border);
border-radius: 6px;
padding: 15px;
min-height: 60px;
font-family: 'Courier New', monospace;
white-space: pre-wrap;
word-wrap: break-word;
overflow-x: auto;
color: var(--text-color);
}
.btn {
background: var(--success-color);
color: #fff;
border: none;
padding: 10px 20px;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
margin-top: 10px;
transition: background 0.2s;
}
.btn:hover { background: #45a049; }
.btn:disabled { background: #ccc; cursor: not-allowed; }
.btn-next { background: var(--highlight-color); }
.btn-next:hover { background: #f57c00; }
/* Footer */
footer {
background: var(--sidebar-bg);
border-top: 1px solid var(--sidebar-border);
padding: 10px 20px;
text-align: right;
font-size: 12px;
color: #888;
}
.progress-bar {
display: flex;
gap: 2px;
margin: 10px 0;
}
.progress-step {
width: 30px;
height: 4px;
background: var(--cmd-bg);
border-radius: 2px;
}
.progress-step.done { background: var(--success-color); }
.progress-step.current { background: var(--highlight-color); }
</style>
</head>
<body>
<header>
<h1>💻 Linux 命令沙盒练习</h1>
<div class="header-actions">
<button class="theme-toggle" onclick="showWrongAnswers()" title="错题本">📚 错题本</button>
<button class="theme-toggle" onclick="toggleTheme()" title="切换深色/浅色模式">
<span id="themeLabel">☀️</span> <span id="themeLabelText">浅色</span>
</button>
<span id="userStatus">未登录</span>
<button id="logoutBtn" class="logout-btn" onclick="logout()" style="display:none">👋 退出</button>
</div>
</header>
<div class="container">
<aside class="sidebar">
<div id="sidebarContent">
<!-- 课程目录将由 JS 动态生成 -->
<p style="text-align:center; color:#888; margin-top:20px;">加载课程中...</p>
</div>
</aside>
<main class="main-content">
<div id="taskPanel" class="task-card" style="display:none">
<h2 class="task-title" id="taskTitle"></h2>
<div class="progress-bar" id="progressBar"></div>
<p class="task-hint" id="taskHint"></p>
<div class="task-desc" id="taskDesc"></div>
<div class="task-success" id="taskSuccess">
<h4>✅ 回答正确!🎉</h4>
<p id="taskSuccessMsg"></p>
</div>
<div class="cmd-area">
<div class="cmd-bar">
<span class="prompt">$</span>
<input type="text" id="cmd" placeholder="输入 Linux 命令..." autocomplete="off" autofocus>
</div>
<div id="output">等待输入命令...</div>
<button id="submitBtn" class="btn" onclick="runCommand()">执行命令</button>
<button id="nextBtn" class="btn btn-next" onclick="nextLevel()" style="display:none">下一关 →</button>
</div>
</div>
<div id="welcomePanel" class="task-card" style="text-align:center; padding:40px 20px;">
<h2>欢迎来到 Linux 命令沙盒练习平台!</h2>
<p style="font-size:18px; margin:20px 0;">这是一个零风险的 Linux 命令学习环境,你可以安全地练习常用命令。</p>
<p><strong>📌 使用说明:</strong></p>
<ul style="text-align:left; display:inline-block;">
<li>左侧是课程目录,按顺序学习</li>
<li>阅读任务描述,输入对应命令</li>
<li>答对后自动解锁下一关</li>
<li>支持深色/浅色主题切换</li>
</ul>
<button class="btn" style="margin-top:30px;" onclick="startFirstLesson()">开始学习 →</button>
</div>
</main>
</div>
<footer>
<span id="userBadge">🔒 未认证</span> |
<span id="currentLevel">Lv.1</span> |
<a href="https://github.com/" target="_blank">Powered by Linux Sandbox</a>
</footer>
<script>
// ==============================
// 全局状态
// ==============================
let currentToken = localStorage.getItem('linux_sandbox_token') || '';
let currentUser = localStorage.getItem('linux_sandbox_user') || '';
let currentTheme = localStorage.getItem('linux_sandbox_theme') || 'light';
let currentTaskId = null;
let completedTasks = JSON.parse(localStorage.getItem('linux_sandbox_completed') || '[]');
let wrongAnswers = JSON.parse(localStorage.getItem('linux_sandbox_wrong' || '[]');
let COURSE_DATA = null; // 将从 /api/tasks 动态加载
// ==============================
// 主题切换
// ==============================
function applyTheme(theme) {
document.documentElement.setAttribute('data-theme', theme);
localStorage.setItem('linux_sandbox_theme', theme);
document.getElementById('themeLabel').textContent = theme === 'dark' ? '🌙' : '☀️';
document.getElementById('themeLabelText').textContent = theme === 'dark' ? '深色' : '浅色';
}
function toggleTheme() {
const newTheme = currentTheme === 'dark' ? 'light' : 'dark';
applyTheme(newTheme);
currentTheme = newTheme;
}
applyTheme(currentTheme);
// ==============================
// 登录/登出
// ==============================
async function login() {
// 建议:实际从登录弹窗调用,这里简化直接用 token
if (currentToken && currentUser) {
updateUI();
return;
}
alert('请先登录!当前简化版支持直接使用内置 Token 执行命令。');
// 临时允许
currentToken = 'safe_linux_2026';
currentUser = 'sandbox_user';
localStorage.setItem('linux_sandbox_token', currentToken);
localStorage.setItem('linux_sandbox_user', currentUser);
logoutBtn.style.display = 'inline-block';
updateUI();
}
function logout() {
localStorage.removeItem('linux_sandbox_token');
localStorage.removeItem('linux_sandbox_user');
currentToken = '';
currentUser = '';
logoutBtn.style.display = 'none';
updateUI();
}
function updateUI() {
const userStatus = document.getElementById('userStatus');
const userBadge = document.getElementById('userBadge');
if (currentToken && currentUser) {
userStatus.textContent = `👤 ${currentUser}`;
userBadge.textContent = '✅ 已认证';
document.getElementById('logoutBtn').style.display = 'inline-block';
} else {
userStatus.textContent = '未登录';
userBadge.textContent = '🔒 未认证';
}
}
// ==============================
// 课程渲染
// ==============================
function renderSidebar() {
if (!COURSE_DATA || !COURSE_DATA.levels) {
console.log("课程数据尚未加载");
return;
}
const sidebar = document.getElementById('sidebarContent');
let html = '<ul class="lesson-list">';
COURSE_DATA.levels.forEach(level => {
html += `<h2>${level.title}</h2>`;
level.challenges.forEach((task, idx) => {
const done = completedTasks.includes(task.id);
const active = task.id === currentTaskId ? 'active' : '';
const locked = !done && idx > 0 && !completedTasks.includes(level.challenges[idx-1].id);
html += `<li class="lesson-item ${active} ${locked ? 'locked' : ''}" onclick="loadTask('${task.id}')">
<span>${idx+1}. ${task.title}</span>
<span class="lesson-badge">${done ? '✅' : '🔒'}</span>
</li>`;
});
});
html += '</ul>';
sidebar.innerHTML = html;
}
// ==============================
// 任务加载
// ==============================
function loadTask(taskId) {
// 检查前序任务是否完成
let prevTaskId = null;
for (const level of COURSE_DATA.levels) {
for (const task of level.challenges) {
if (task.id === taskId) {
if (prevTaskId && !completedTasks.includes(prevTaskId)) {
alert('请先完成前一关!');
return;
}
currentTaskId = taskId;
renderTask(task);
renderSidebar(); // 更新高亮
return;
}
prevTaskId = task.id;
}
}
alert('任务未找到');
}
function renderTask(task) {
document.getElementById('welcomePanel').style.display = 'none';
document.getElementById('taskPanel').style.display = 'block';
document.getElementById('taskTitle').innerHTML = task.title;
document.getElementById('taskDesc').innerHTML = task.description;
document.getElementById('taskHint').textContent = `💡 ${task.hint}`;
document.getElementById('taskSuccess').classList.remove('show');
document.getElementById('taskSuccessMsg').textContent = task.success_msg || '恭喜你,任务完成!';
document.getElementById('submitBtn').style.display = 'inline-block';
document.getElementById('nextBtn').style.display = 'none';
// 渲染进度条
const progressBar = document.getElementById('progressBar');
let progressHtml = '';
for (let i=0; i<5; i++) {
progressHtml += `<div class="progress-step ${i === 0 ? 'current' : ''} ${i < 0 ? 'done' : ''}"></div>`;
}
progressBar.innerHTML = progressHtml;
// 聚焦输入框
document.getElementById('cmd').value = '';
document.getElementById('cmd').focus();
}
function startFirstLesson() {
if (!currentToken) {
login();
}
if (!COURSE_DATA) {
alert('课程数据加载中,请稍候...');
return;
}
loadTask(COURSE_DATA.levels[0].challenges[0].id);
}
// ==============================
// 命令执行
// ==============================
async function runCommand() {
const cmd = document.getElementById('cmd').value.trim();
if (!cmd) return;
const output = document.getElementById('output');
output.textContent = `执行: ${cmd}\n正在运行...`;
document.getElementById('submitBtn').disabled = true;
document.getElementById('cmd').disabled = true;
try {
const res = await fetch('/api/run?cmd=' + encodeURIComponent(cmd));
const data = await res.json();
output.textContent = data.output || data.message || '(无输出)';
// 检查任务是否完成
checkTaskCompletion(cmd, data.output || '');
} catch (e) {
output.textContent = `❌ 错误: ${e.message}`;
} finally {
document.getElementById('submitBtn').disabled = false;
document.getElementById('cmd').disabled = false;
}
}
function checkTaskCompletion(cmd, output) {
const task = findTaskById(currentTaskId);
if (!task) return;
// 检查 solution 匹配
const solutionMatch = task.success_test || (task.solution && task.solution.includes(cmd));
if (solutionMatch || task.solution?.includes(cmd)) {
handleTaskSuccess(task);
} else {
// 简单启发式检查(未实现 full success_test 解析)
console.log('需要完成任务检查逻辑');
recordWrongAnswer(currentTaskId, cmd, output);
}
}
function handleTaskSuccess(task) {
if (!completedTasks.includes(task.id)) {
completedTasks.push(task.id);
localStorage.setItem('linux_sandbox_completed', JSON.stringify(completedTasks));
alert('✅ 回答正确!\n奖励经验值 +100');
}
document.getElementById('submitBtn').style.display = 'none';
const successBox = document.getElementById('taskSuccess');
successBox.querySelector('h4').innerHTML = '🎉 回答正确!';
successBox.querySelector('p').textContent = task.success_msg || '你已经掌握这个命令的基本用法!';
successBox.classList.add('show');
document.getElementById('nextBtn').style.display = 'inline-block';
}
function nextLevel() {
const task = findTaskById(currentTaskId);
if (!task) return;
// 找到下一关
let found = false;
for (const level of COURSE_DATA.levels) {
for (const t of level.challenges) {
if (found) {
loadTask(t.id);
return;
}
if (t.id === task.id) found = true;
}
}
alert('🎉 所有课程已完成!你已经是 Linux 大师了!🏆');
}
function findTaskById(id) {
for (const level of COURSE_DATA.levels) {
const task = level.challenges.find(t => t.id === id);
if (task) return task;
}
return null;
}
// ==============================
// 事件监听
// ==============================
document.getElementById('cmd').addEventListener('keypress', function(e) {
if (e.key === 'Enter') runCommand();
});
// ==============================
// 初始化
// ==============================
function init() {
updateUI();
// 渲染侧边栏(课程数据加载后调用 renderSidebar
if (COURSE_DATA && COURSE_DATA.levels) renderSidebar();
// 检查是否已有学习进度
if (currentToken && !currentTaskId && !completedTasks.length) {
// 从第一关开始(延迟加载)
setTimeout(startFirstLesson, 1000);
} else if (currentTaskId) {
const task = findTaskById(currentTaskId);
if (task) renderTask(task);
}
}
// 等待课程数据加载完成后初始化 UI
let dataLoaded = false;
setInterval(() => {
if (COURSE_DATA && !dataLoaded) {
init();
dataLoaded = true;
}
}, 500);
// ==============================
// 加载课程数据API
// ==============================
async function loadCourseData() {
try {
const res = await fetch('/api/tasks');
if (res.ok) {
COURSE_DATA = await res.json();
console.log('✅ 课程数据加载成功', COURSE_DATA);
renderSidebar();
} else {
console.error('❌ 课程数据加载失败:', res.statusText);
}
} catch (e) {
console.error('❌ 课程数据获取异常:', e);
}
}
// 页面全局函数(供 onclick 调用)
window.loadTask = loadTask;
window.runCommand = runCommand;
window.nextLevel = nextLevel;
window.startFirstLesson = startFirstLesson;
// ==============================
// 错题记录功能
// ==============================
function recordWrongAnswer(taskId, cmd, output) {
const wrongItem = {
taskId: taskId,
cmd: cmd,
output: output,
timestamp: new Date().toISOString(),
count: 1
};
// 检查是否已存在
const existing = wrongAnswers.find(w => w.taskId === taskId);
if (existing) {
existing.count++;
existing.cmd = cmd;
existing.output = output;
existing.timestamp = wrongItem.timestamp;
} else {
wrongAnswers.push(wrongItem);
}
localStorage.setItem('linux_sandbox_wrong', JSON.stringify(wrongAnswers));
}
function showWrongAnswers() {
if (wrongAnswers.length === 0) {
alert('🎉 还没有错题记录!继续保持!');
return;
}
let html = '<h3>📚 错题本</h3><ul>';
wrongAnswers.forEach(w => {
const task = findTaskById(w.taskId);
const title = task ? task.title : w.taskId;
html += `<li style="margin:10px 0;padding:10px;background:#fff3e0;border-radius:4px;">
<strong>${title}</strong> (错误 ${w.count} 次)<br>
<code>${w.cmd}</code><br>
<small>${new Date(w.timestamp).toLocaleString()}</small>
</li>`;
});
html += '</ul>';
// 创建弹窗显示
const modal = document.createElement('div');
modal.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.5);z-index:1000;display:flex;align-items:center;justify-content:center;';
modal.innerHTML = `<div style="background:white;padding:20px;border-radius:8px;max-width:600px;max-height:80vh;overflow:auto;">${html}<br><button onclick="this.parentElement.parentElement.remove()" style="padding:10px 20px;background:#4caf50;color:white;border:none;border-radius:4px;cursor:pointer;">关闭</button></div>`;
document.body.appendChild(modal);
}
function clearWrongAnswers() {
if (confirm('确定要清空所有错题记录吗?')) {
wrongAnswers = [];
localStorage.removeItem('linux_sandbox_wrong');
alert('✅ 错题记录已清空');
}
}
// 立即加载课程数据
document.addEventListener('DOMContentLoaded', loadCourseData);
</script>
</body>
</html>