2026-03-07 05:43:51 +00:00
<!DOCTYPE html>
< html lang = "zh-CN" >
< head >
2026-03-10 07:30:42 +08:00
< 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: #1677ff;
--primary-dark: #0f5ed7;
--primary-soft: #eef5ff;
--secondary: #36a3ff;
--accent: #22c55e;
--warning: #f59e0b;
--danger: #ef4444;
--bg: #f4f8ff;
--bg-2: #edf4ff;
--card: rgba(255,255,255,0.95);
--text: #0f172a;
--text-2: #334155;
--text-3: #64748b;
--border: #dbe7f5;
--terminal: #0b1220;
--terminal-panel: #111b2e;
--terminal-text: #d8e7ff;
--shadow: 0 18px 45px rgba(15, 94, 215, 0.12);
--radius: 20px;
}
[data-theme="dark"] {
--bg: #09111f;
--bg-2: #0e1930;
--card: rgba(17, 27, 46, 0.95);
--text: #e8f0ff;
--text-2: #cbd8f0;
--text-3: #8ea2c8;
--border: #1d3358;
--terminal: #050a13;
--terminal-panel: #0b1220;
--terminal-text: #d8e7ff;
--shadow: 0 20px 55px rgba(2, 8, 23, 0.45);
}
body {
font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Microsoft YaHei', sans-serif;
background: linear-gradient(180deg, var(--bg) 0%, var(--bg-2) 100%);
color: var(--text);
min-height: 100vh;
}
.header {
background: linear-gradient(135deg, #0f5ed7 0%, #1677ff 48%, #36a3ff 100%);
color: #fff;
padding: 18px 24px;
box-shadow: 0 12px 35px rgba(22, 119, 255, 0.2);
}
.header-content {
max-width: 1440px;
margin: 0 auto;
display: flex;
justify-content: space-between;
align-items: center;
gap: 20px;
}
.brand h1 {
font-size: 24px;
font-weight: 800;
letter-spacing: 0.02em;
}
.brand p {
font-size: 13px;
opacity: 0.9;
margin-top: 4px;
}
.header-actions { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }
.pill-btn {
border: none;
padding: 10px 16px;
border-radius: 999px;
background: rgba(255,255,255,0.16);
color: #fff;
cursor: pointer;
font-weight: 600;
transition: all .2s ease;
}
.pill-btn:hover, .pill-btn.active { background: #fff; color: var(--primary); }
.layout {
max-width: 1440px;
margin: 0 auto;
padding: 20px;
display: grid;
grid-template-columns: 300px minmax(0, 1fr) 360px;
gap: 18px;
min-height: calc(100vh - 92px);
}
.card {
background: var(--card);
border: 1px solid var(--border);
border-radius: var(--radius);
box-shadow: var(--shadow);
backdrop-filter: blur(10px);
}
.sidebar, .learning-panel { padding: 18px; }
.section-title {
font-size: 16px;
font-weight: 800;
margin-bottom: 14px;
color: var(--text);
display: flex;
align-items: center;
gap: 8px;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 10px;
margin-bottom: 16px;
}
.stat-box {
background: linear-gradient(180deg, var(--primary-soft) 0%, rgba(255,255,255,0.7) 100%);
border: 1px solid var(--border);
border-radius: 16px;
padding: 14px;
}
.stat-box .label { font-size: 12px; color: var(--text-3); }
.stat-box .value { font-size: 24px; font-weight: 800; margin-top: 6px; }
.progress-wrap { margin-bottom: 16px; }
.progress-bar {
height: 10px; background: rgba(148,163,184,0.15); border-radius: 999px; overflow: hidden;
}
.progress-fill {
height: 100%; background: linear-gradient(90deg, var(--primary), var(--secondary)); transition: width .3s ease;
}
.progress-text { margin-top: 8px; font-size: 13px; color: var(--text-3); }
.level-header {
display: flex; justify-content: space-between; align-items: center; cursor: pointer;
padding: 12px 14px; margin-bottom: 10px; border-radius: 14px;
background: linear-gradient(90deg, #eef6ff 0%, #f8fbff 100%); color: var(--primary-dark); font-weight: 700;
border: 1px solid #d8e7fb;
}
.task-list { list-style: none; margin-bottom: 12px; }
.task-item {
padding: 12px 14px; border-radius: 14px; display: flex; gap: 10px; align-items: start; cursor: pointer;
transition: all .2s ease; color: var(--text-2); margin-bottom: 8px;
}
.task-item:hover { background: var(--primary-soft); }
.task-item.active { background: linear-gradient(90deg, var(--primary) 0%, var(--secondary) 100%); color: #fff; }
.task-item.completed:not(.active) { background: rgba(34,197,94,.08); }
.task-status { font-size: 16px; line-height: 1.2; }
.main-panel { padding: 20px; display: flex; flex-direction: column; gap: 18px; }
.hero {
padding: 26px; border-radius: 24px;
background: linear-gradient(135deg, rgba(22,119,255,.12), rgba(54,163,255,.1));
border: 1px solid #d8e7fb;
}
.hero h2 { font-size: 30px; margin-bottom: 10px; }
.hero p { color: var(--text-3); line-height: 1.8; }
.hero-actions { display: flex; gap: 12px; flex-wrap: wrap; margin-top: 18px; }
.btn {
border: none; border-radius: 14px; padding: 12px 18px; cursor: pointer; font-size: 14px; font-weight: 700;
transition: all .2s ease;
}
.btn-primary { background: linear-gradient(135deg, var(--primary), var(--secondary)); color: #fff; }
.btn-primary:hover { transform: translateY(-1px); }
.btn-secondary { background: #edf3ff; color: var(--primary-dark); }
.btn-warning { background: #fff4df; color: #b76a00; }
.task-shell { display: none; flex-direction: column; gap: 18px; }
.task-shell.show { display: flex; }
.task-meta {
display: flex; gap: 10px; flex-wrap: wrap; color: var(--text-3); font-size: 13px;
}
.badge {
display: inline-flex; align-items: center; padding: 6px 10px; border-radius: 999px; font-size: 12px; font-weight: 700;
background: var(--primary-soft); color: var(--primary-dark);
}
.task-desc {
padding: 18px; border-radius: 18px; background: linear-gradient(180deg, #f8fbff 0%, #f1f7ff 100%);
border: 1px solid #dce9f8; line-height: 1.8;
}
.terminal {
background: linear-gradient(180deg, var(--terminal) 0%, var(--terminal-panel) 100%);
border-radius: 22px; padding: 16px; color: var(--terminal-text); border: 1px solid rgba(59,130,246,.18);
}
.terminal-top { display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px; }
.terminal-title { font-weight: 700; color: #b9d6ff; }
.cwd-chip {
background: rgba(22,119,255,.18); border: 1px solid rgba(96,165,250,.28); color: #dbeafe;
font-size: 12px; padding: 6px 10px; border-radius: 999px;
}
.prompt-line {
display: flex; align-items: center; gap: 10px; padding: 12px 14px;
border-radius: 14px; background: rgba(255,255,255,.03); border: 1px solid rgba(96,165,250,.15);
}
.prompt { color: #4ade80; font-weight: 700; font-family: Consolas, monospace; }
.command-input {
flex: 1; background: transparent; border: none; color: #fff; outline: none; font-family: Consolas, monospace; font-size: 15px;
}
.terminal-output {
margin-top: 12px; min-height: 170px; max-height: 380px; overflow: auto;
border-radius: 14px; padding: 14px; background: rgba(2, 6, 23, 0.45);
font-family: Consolas, monospace; white-space: pre-wrap; line-height: 1.6;
}
.feedback {
display: none; padding: 16px 18px; border-radius: 16px; font-size: 14px; font-weight: 600;
}
.feedback.show { display: block; }
.feedback.success { background: rgba(34,197,94,.1); color: #15803d; border: 1px solid rgba(34,197,94,.2); }
.feedback.warn { background: rgba(245,158,11,.1); color: #b45309; border: 1px solid rgba(245,158,11,.2); }
.feedback.error { background: rgba(239,68,68,.1); color: #b91c1c; border: 1px solid rgba(239,68,68,.2); }
.action-row { display: flex; gap: 12px; flex-wrap: wrap; }
.learning-panel .hint-card,
.learning-panel .knowledge-card,
.learning-panel .milestone-card {
padding: 16px; border-radius: 16px; margin-bottom: 14px; border: 1px solid var(--border);
}
.hint-card { background: linear-gradient(180deg, #f8fbff 0%, #f3f8ff 100%); }
.knowledge-card { background: linear-gradient(180deg, #eef6ff 0%, #f8fbff 100%); }
.milestone-card { background: linear-gradient(180deg, rgba(34,197,94,.08), rgba(255,255,255,.9)); }
.small-title { font-size: 13px; font-weight: 800; color: var(--text-2); margin-bottom: 8px; }
.code-list { display: flex; flex-direction: column; gap: 8px; }
.code-chip {
display: block; padding: 10px 12px; border-radius: 12px; background: rgba(15, 23, 42, 0.04); font-family: Consolas, monospace;
color: var(--primary-dark); font-size: 13px;
}
.empty-state { color: var(--text-3); line-height: 1.8; }
@media (max-width: 1240px) {
.layout { grid-template-columns: 280px minmax(0,1fr); }
.learning-panel { display: none; }
}
@media (max-width: 820px) {
.layout { grid-template-columns: 1fr; }
.sidebar { order: 2; }
.main-panel { order: 1; }
.header-content { flex-direction: column; align-items: start; }
}
< / style >
2026-03-07 05:43:51 +00:00
< / head >
< body >
2026-03-10 07:30:42 +08:00
< header class = "header" >
< div class = "header-content" >
< div class = "brand" >
< h1 > 🐧 Linux 运维学习平台< / h1 >
< p > 内容更系统 · 练习更真实 · 判题更可靠< / p >
< / div >
< div class = "header-actions" >
< button class = "pill-btn active" id = "learnMode" onclick = "setMode('learn')" > 📖 学习模式< / button >
< button class = "pill-btn" id = "practiceMode" onclick = "setMode('practice')" > ⚔️ 实战模式< / button >
< button class = "pill-btn" onclick = "resetSandbox()" > ♻️ 重置环境< / button >
< button class = "pill-btn" onclick = "toggleTheme()" > 🌓 主题< / button >
< / div >
< / div >
< / header >
< div class = "layout" >
< aside class = "sidebar card" >
< div class = "section-title" > 📚 学习地图< / div >
< div class = "stats-grid" >
< div class = "stat-box" >
< div class = "label" > 课程关卡< / div >
< div class = "value" id = "levelCount" > 12< / div >
2026-03-07 05:43:51 +00:00
< / div >
2026-03-10 07:30:42 +08:00
< div class = "stat-box" >
< div class = "label" > 总任务数< / div >
< div class = "value" id = "taskCount" > 80< / div >
< / div >
< div class = "stat-box" >
< div class = "label" > 已完成< / div >
< div class = "value" id = "doneCount" > 0< / div >
< / div >
< div class = "stat-box" >
< div class = "label" > 当前模式< / div >
< div class = "value" id = "modeLabel" style = "font-size:18px" > 学习< / div >
< / div >
< / div >
< div class = "progress-wrap" >
< div class = "progress-bar" > < div class = "progress-fill" id = "progressFill" style = "width:0%" > < / div > < / div >
< div class = "progress-text" id = "progressText" > 0 / 80 完成 (0%)< / div >
< / div >
< div id = "courseNav" > < / div >
< / aside >
2026-03-07 05:43:51 +00:00
2026-03-10 07:30:42 +08:00
< main class = "main-panel card" >
< section class = "hero" id = "heroPanel" >
< h2 > 从命令入门,到运维思维< / h2 >
< p >
这个版本不再只是“输入答案过题”。你现在会得到更真实的沙盒环境、状态可变的文件系统、
更合理的后端判题,以及更清晰的学习建议。
< / p >
< div class = "hero-actions" >
< button class = "btn btn-primary" onclick = "startLearning()" > 开始第一关< / button >
< button class = "btn btn-secondary" onclick = "jumpToFirstUnfinished()" > 继续上次进度< / button >
< / div >
< / section >
2026-03-07 05:43:51 +00:00
2026-03-10 07:30:42 +08:00
< section class = "task-shell" id = "taskShell" >
< div >
< div class = "task-meta" >
< span class = "badge" id = "taskLevel" > Level 1< / span >
< span class = "badge" id = "taskIndex" > 1 / 80< / span >
< span class = "badge" id = "taskModeBadge" > 当前:学习模式< / span >
< / div >
< h2 style = "margin-top:12px; font-size:28px;" id = "taskTitle" > 任务标题< / h2 >
< / div >
2026-03-07 05:43:51 +00:00
2026-03-10 07:30:42 +08:00
< div class = "task-desc" id = "taskDescription" > < / div >
2026-03-07 05:43:51 +00:00
2026-03-10 07:30:42 +08:00
< div class = "terminal" >
< div class = "terminal-top" >
< div class = "terminal-title" > 🖥️ Linux 终端< / div >
< div class = "cwd-chip" id = "cwdChip" > 当前目录:/< / div >
< / div >
< div class = "prompt-line" >
< span class = "prompt" > sandbox@linux:$< / span >
< input id = "cmdInput" class = "command-input" placeholder = "输入 Linux 命令,例如 ls -la /etc" onkeypress = "if(event.key==='Enter') executeCommand()" / >
< / div >
< div class = "terminal-output" id = "cmdOutput" > 欢迎进入 Linux 沙盒。你可以安全练习命令,不会影响真实系统。< / div >
< / div >
2026-03-07 05:43:51 +00:00
2026-03-10 07:30:42 +08:00
< div class = "feedback" id = "feedbackBox" > < / div >
2026-03-07 05:43:51 +00:00
2026-03-10 07:30:42 +08:00
< div class = "action-row" >
< button class = "btn btn-primary" onclick = "executeCommand()" > ▶ 执行命令< / button >
< button class = "btn btn-secondary" onclick = "showHint()" > 💡 提示< / button >
< button class = "btn btn-warning" onclick = "showAnswer()" > 👀 查看答案< / button >
< button class = "btn btn-secondary" onclick = "nextTask()" > 下一题 →< / button >
< / div >
< / section >
< / main >
2026-03-07 05:43:51 +00:00
2026-03-10 07:30:42 +08:00
< aside class = "learning-panel card" >
< div class = "section-title" > 🧠 学习辅助< / div >
< div class = "hint-card" >
< div class = "small-title" > 本题提示< / div >
< div id = "hintBox" class = "empty-state" > 选择一题后,这里会显示更聚焦的提示。< / div >
< / div >
< div class = "knowledge-card" >
< div class = "small-title" > 命令讲解< / div >
< div id = "knowledgeBox" class = "empty-state" > 选择任务后,这里会提供命令用途、示例与注意点。< / div >
< / div >
< div class = "milestone-card" >
< div class = "small-title" > 当前目标< / div >
< div id = "milestoneBox" class = "empty-state" > 开始第一关,逐步建立 Linux 运维基本功。< / div >
< / div >
< / aside >
< / div >
2026-03-07 05:43:51 +00:00
2026-03-10 07:30:42 +08:00
< script >
let COURSE_DATA = null;
let currentTask = null;
let currentMode = localStorage.getItem('linux_mode') || 'learn';
let currentTheme = localStorage.getItem('linux_theme') || 'light';
let completedTasks = JSON.parse(localStorage.getItem('linux_completed') || '[]');
let currentCwd = '/';
2026-03-07 05:43:51 +00:00
2026-03-10 07:30:42 +08:00
const COMMAND_KNOWLEDGE = {
pwd: {
desc: 'pwd 用来显示当前工作目录,是你在文件系统中的当前位置。',
examples: ['pwd', 'cd /tmp & & pwd'],
tip: '一旦不知道自己在哪,先敲 pwd。'
},
ls: {
desc: 'ls 用于查看目录内容。-l 看详细信息,-a 看隐藏文件。',
examples: ['ls', 'ls -la /etc', 'ls -lh /var/log'],
tip: 'ls -la 是最常用组合。'
},
cd: {
desc: 'cd 用于切换目录,配合 pwd 使用能快速建立路径感。',
examples: ['cd /tmp', 'cd ..', 'cd ~'],
tip: 'cd .. 返回上级目录, cd ~ 回主目录。'
},
cat: {
desc: 'cat 直接输出文件内容,适合看短文件。大文件更适合 less。',
examples: ['cat /etc/passwd', 'cat -n /etc/hosts'],
tip: '看配置文件或小文本时很顺手。'
},
grep: {
desc: 'grep 用于文本搜索,是日志排障和配置定位的核心命令。',
examples: ['grep root /etc/passwd', 'grep -in error /var/log/syslog'],
tip: '日志场景优先想到 grep -n / grep -i。'
},
find: {
desc: 'find 用于按名字、类型、大小等条件查找文件。',
examples: ["find /etc -name '*.conf'", 'find /tmp -type d'],
tip: '排查文件位置时, find 非常强。'
},
mkdir: {
desc: 'mkdir 用于创建目录。-p 可以递归创建多级目录。',
examples: ['mkdir /tmp/demo', 'mkdir -p /tmp/a/b/c'],
tip: '多级目录优先加 -p。'
},
chmod: {
desc: 'chmod 用于改权限,是 Linux 文件安全基础。',
examples: ['chmod 755 script.sh', 'chmod +x run.sh'],
tip: '755 给目录或脚本很常见。'
},
ps: {
desc: 'ps 用于查看进程,运维定位服务状态离不开它。',
examples: ['ps', 'ps aux', 'ps -ef'],
tip: 'ps aux | grep 进程名 是经典组合。'
}
};
2026-03-07 05:43:51 +00:00
2026-03-10 07:30:42 +08:00
document.addEventListener('DOMContentLoaded', async () => {
applyTheme(currentTheme);
setMode(currentMode, false);
await loadCourseData();
renderCourseNav();
updateStats();
});
2026-03-07 05:43:51 +00:00
2026-03-10 07:30:42 +08:00
async function loadCourseData() {
const res = await fetch('/api/tasks');
COURSE_DATA = await res.json();
document.getElementById('levelCount').textContent = COURSE_DATA.meta.total_levels || COURSE_DATA.levels.length;
document.getElementById('taskCount').textContent = getAllTasks().length;
}
2026-03-07 05:43:51 +00:00
2026-03-10 07:30:42 +08:00
function getAllTasks() {
return COURSE_DATA ? COURSE_DATA.levels.flatMap(level => level.challenges.map(task => ({...task, levelTitle: level.title, levelId: level.id}))) : [];
}
2026-03-07 05:43:51 +00:00
2026-03-10 07:30:42 +08:00
function setMode(mode, save = true) {
currentMode = mode;
document.getElementById('learnMode').classList.toggle('active', mode === 'learn');
document.getElementById('practiceMode').classList.toggle('active', mode === 'practice');
document.getElementById('modeLabel').textContent = mode === 'learn' ? '学习' : '实战';
document.getElementById('taskModeBadge').textContent = `当前:${mode === 'learn' ? '学习模式' : '实战模式'}`;
if (save) localStorage.setItem('linux_mode', mode);
if (currentTask) renderTask(currentTask);
}
2026-03-07 05:43:51 +00:00
2026-03-10 07:30:42 +08:00
function toggleTheme() {
currentTheme = currentTheme === 'light' ? 'dark' : 'light';
applyTheme(currentTheme);
localStorage.setItem('linux_theme', currentTheme);
}
2026-03-07 05:43:51 +00:00
2026-03-10 07:30:42 +08:00
function applyTheme(theme) {
document.documentElement.setAttribute('data-theme', theme);
}
2026-03-07 05:43:51 +00:00
2026-03-10 07:30:42 +08:00
function renderCourseNav() {
if (!COURSE_DATA) return;
const nav = document.getElementById('courseNav');
nav.innerHTML = COURSE_DATA.levels.map((level, levelIndex) => {
const items = level.challenges.map(task => {
const active = currentTask & & currentTask.id === task.id;
const completed = completedTasks.includes(task.id);
return `
< li class = "task-item ${active ? 'active' : ''} ${completed ? 'completed' : ''}" onclick = "selectTask('${level.id}','${task.id}')" >
< span class = "task-status" > ${completed ? '✅' : (active ? '▶' : '○')}< / span >
< div >
< div style = "font-weight:700;" > ${task.title}< / div >
< div style = "font-size:12px; opacity:.85; margin-top:3px;" > ${(task.description || task.desc || '').slice(0, 28)}...< / div >
< / div >
< / li > `;
}).join('');
return `
< div style = "margin-bottom:12px;" >
< div class = "level-header" onclick = "toggleLevel(${levelIndex})" >
< span > ${level.title}< / span >
< span > 共 ${level.challenges.length} 题< / span >
< / div >
< ul class = "task-list" id = "level-${levelIndex}" > ${items}< / ul >
< / div > `;
}).join('');
}
2026-03-07 05:43:51 +00:00
2026-03-10 07:30:42 +08:00
function toggleLevel(index) {
const el = document.getElementById(`level-${index}`);
el.style.display = el.style.display === 'none' ? 'block' : 'none';
}
2026-03-07 05:43:51 +00:00
2026-03-10 07:30:42 +08:00
function selectTask(levelId, taskId) {
const level = COURSE_DATA.levels.find(l => l.id === levelId);
const flat = getAllTasks();
const index = flat.findIndex(t => t.id === taskId);
const task = level.challenges.find(t => t.id === taskId);
currentTask = { ...task, levelTitle: level.title, levelId: level.id, globalIndex: index + 1, totalCount: flat.length };
renderTask(currentTask);
renderCourseNav();
}
2026-03-07 05:43:51 +00:00
2026-03-10 07:30:42 +08:00
function renderTask(task) {
document.getElementById('heroPanel').style.display = 'none';
document.getElementById('taskShell').classList.add('show');
document.getElementById('taskTitle').textContent = task.title;
document.getElementById('taskLevel').textContent = task.levelTitle;
document.getElementById('taskIndex').textContent = `${task.globalIndex} / ${task.totalCount}`;
document.getElementById('taskModeBadge').textContent = `当前:${currentMode === 'learn' ? '学习模式' : '实战模式'}`;
document.getElementById('hintBox').textContent = task.hint || '先思考命令目标,再尝试最短的正确命令。';
document.getElementById('milestoneBox').innerHTML = `当前目标:< strong > ${task.title}< / strong > < br / > 完成后将解锁后续任务。`;
currentCwd = '/';
updateCwd(task.cwd || currentCwd);
const desc = task.description || task.desc || '完成本题要求。';
document.getElementById('taskDescription').innerHTML = `
< div style = "font-size:15px; color:var(--text-2);" > ${desc}< / div >
${currentMode === 'learn' ? `< div style = "margin-top:14px; padding:12px 14px; border-radius:14px; background:rgba(22,119,255,.08); color:var(--text-2);" > < strong > 学习提示:< / strong > ${task.hint || '试着从命令用途出发。'}< / div > ` : ''}
`;
document.getElementById('cmdInput').value = '';
document.getElementById('cmdOutput').textContent = '准备好了就开始输入命令。';
hideFeedback();
updateKnowledgePanel(task);
}
function updateKnowledgePanel(task) {
const text = task.description || task.desc || '';
const match = text.match(/< code > ([\w-]+)< \/code>/) || text.match(/使用\s+([\w-]+)/);
const cmd = match ? match[1] : null;
const box = document.getElementById('knowledgeBox');
if (cmd & & COMMAND_KNOWLEDGE[cmd]) {
const info = COMMAND_KNOWLEDGE[cmd];
box.innerHTML = `
< div style = "font-weight:800; color:var(--primary-dark); margin-bottom:8px;" > ${cmd}< / div >
< div style = "color:var(--text-2); line-height:1.8; margin-bottom:12px;" > ${info.desc}< / div >
< div class = "code-list" > ${info.examples.map(item => `< span class = "code-chip" > ${item}< / span > `).join('')}< / div >
< div style = "margin-top:12px; font-size:13px; color:var(--text-3);" > 💡 ${info.tip}< / div >
`;
} else {
box.innerHTML = '< div class = "empty-state" > 这题更偏向综合操作。先理解目标,再尝试一步步拆分命令。< / div > ';
}
}
async function executeCommand() {
const input = document.getElementById('cmdInput');
const cmd = input.value.trim();
if (!cmd || !currentTask) return;
const outputEl = document.getElementById('cmdOutput');
outputEl.textContent = `$ ${cmd}\n\n执行中...`;
hideFeedback();
try {
const res = await fetch('/api/run?cmd=' + encodeURIComponent(cmd));
const data = await res.json();
currentCwd = data.cwd || currentCwd;
updateCwd(currentCwd);
outputEl.textContent = `$ ${cmd}\n\n${data.output || data.message || '(无输出)'}`;
await checkAnswer(cmd, data.output || '');
} catch (e) {
outputEl.textContent = `❌ 执行失败:${e.message}`;
showFeedback('error', '命令执行失败,请稍后重试。');
}
}
async function checkAnswer(cmd, output) {
const res = await fetch(`/api/check?task_id=${encodeURIComponent(currentTask.id)}&last_cmd=${encodeURIComponent(cmd)}&output=${encodeURIComponent(output)}`);
const data = await res.json();
if (data.success) {
if (!completedTasks.includes(currentTask.id)) {
completedTasks.push(currentTask.id);
localStorage.setItem('linux_completed', JSON.stringify(completedTasks));
updateStats();
}
showFeedback('success', data.message + (data.next_suggestion ? `\n${data.next_suggestion}` : ''));
renderCourseNav();
} else {
showFeedback('warn', data.message || '还没通过,再试一次。');
}
}
function showHint() {
if (!currentTask) return;
showFeedback('warn', '提示:' + (currentTask.hint || '从命令的最基础用法开始试。'));
}
function showAnswer() {
if (!currentTask || !currentTask.solution || !currentTask.solution.length) return;
if (!confirm('查看答案会降低训练效果,确定继续?')) return;
document.getElementById('cmdInput').value = currentTask.solution[0];
showFeedback('warn', '标准答案已填入输入框,你可以先自己理解后再执行。');
}
function nextTask() {
const tasks = getAllTasks();
if (!currentTask) return startLearning();
const idx = tasks.findIndex(t => t.id === currentTask.id);
if (idx >= 0 & & idx + 1 < tasks.length ) {
const nxt = tasks[idx + 1];
selectTask(nxt.levelId, nxt.id);
} else {
showFeedback('success', '🎉 你已经完成全部任务,接下来可以复盘和挑战更高难度命令组合。');
}
}
function startLearning() {
const first = getAllTasks()[0];
if (first) selectTask(first.levelId, first.id);
}
function jumpToFirstUnfinished() {
const tasks = getAllTasks();
const first = tasks.find(t => !completedTasks.includes(t.id)) || tasks[0];
if (first) selectTask(first.levelId, first.id);
}
function updateStats() {
const total = getAllTasks().length || 0;
const completed = completedTasks.length;
const percent = total ? Math.round(completed / total * 100) : 0;
document.getElementById('doneCount').textContent = completed;
document.getElementById('progressFill').style.width = percent + '%';
document.getElementById('progressText').textContent = `${completed} / ${total} 完成 (${percent}%)`;
document.getElementById('taskCount').textContent = total;
}
function updateCwd(cwd) {
document.getElementById('cwdChip').textContent = `当前目录:${cwd}`;
}
async function resetSandbox() {
await fetch('/api/reset', { method: 'POST' });
currentCwd = '/';
updateCwd(currentCwd);
document.getElementById('cmdOutput').textContent = '♻️ 沙盒环境已重置。你可以重新挑战当前任务。';
showFeedback('warn', '沙盒环境已重置,目录、文件和权限状态都恢复到初始值。');
}
function showFeedback(type, text) {
const box = document.getElementById('feedbackBox');
box.className = `feedback show ${type}`;
box.textContent = text;
}
function hideFeedback() {
const box = document.getElementById('feedbackBox');
box.className = 'feedback';
box.textContent = '';
}
2026-03-07 05:43:51 +00:00
2026-03-10 07:30:42 +08:00
window.setMode = setMode;
window.toggleTheme = toggleTheme;
window.selectTask = selectTask;
window.toggleLevel = toggleLevel;
window.executeCommand = executeCommand;
window.showHint = showHint;
window.showAnswer = showAnswer;
window.nextTask = nextTask;
window.startLearning = startLearning;
window.resetSandbox = resetSandbox;
window.jumpToFirstUnfinished = jumpToFirstUnfinished;
< / script >
2026-03-07 05:43:51 +00:00
< / body >
< / html >