Files
linux-practice/index.html

641 lines
27 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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: #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>
</head>
<body>
<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>
</div>
<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>
<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>
<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>
<div class="task-desc" id="taskDescription"></div>
<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>
<div class="feedback" id="feedbackBox"></div>
<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>
<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>
<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 = '/';
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 进程名 是经典组合。'
}
};
document.addEventListener('DOMContentLoaded', async () => {
applyTheme(currentTheme);
setMode(currentMode, false);
await loadCourseData();
renderCourseNav();
updateStats();
});
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;
}
function getAllTasks() {
return COURSE_DATA ? COURSE_DATA.levels.flatMap(level => level.challenges.map(task => ({...task, levelTitle: level.title, levelId: level.id}))) : [];
}
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);
}
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');
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('');
}
function toggleLevel(index) {
const el = document.getElementById(`level-${index}`);
el.style.display = el.style.display === 'none' ? 'block' : 'none';
}
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();
}
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 = '';
}
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>
</body>
</html>