feat: redesign linux course around learning-first structure
This commit is contained in:
806
index.html
806
index.html
@@ -3,42 +3,39 @@
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Linux 命令学习平台 - 从入门到精通</title>
|
||||
<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;
|
||||
--accent: #36a3ff;
|
||||
--success: #22c55e;
|
||||
--warning: #f59e0b;
|
||||
--danger: #ef4444;
|
||||
--bg: #f4f8ff;
|
||||
--bg: #f5f9ff;
|
||||
--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;
|
||||
--radius: 22px;
|
||||
--terminal: #0b1220;
|
||||
--terminal-soft: #101a2f;
|
||||
}
|
||||
[data-theme="dark"] {
|
||||
--bg: #09111f;
|
||||
--bg-2: #0e1930;
|
||||
--card: rgba(17, 27, 46, 0.95);
|
||||
--text: #e8f0ff;
|
||||
--text-2: #cbd8f0;
|
||||
--text-3: #8ea2c8;
|
||||
[data-theme='dark'] {
|
||||
--bg: #08101d;
|
||||
--bg-2: #101b31;
|
||||
--card: rgba(16, 26, 47, 0.96);
|
||||
--text: #edf4ff;
|
||||
--text-2: #cfdbf4;
|
||||
--text-3: #90a5ca;
|
||||
--border: #1d3358;
|
||||
--terminal: #050a13;
|
||||
--terminal-panel: #0b1220;
|
||||
--terminal-text: #d8e7ff;
|
||||
--shadow: 0 20px 55px rgba(2, 8, 23, 0.45);
|
||||
--shadow: 0 18px 55px rgba(2, 8, 23, 0.42);
|
||||
--primary-soft: rgba(22,119,255,.12);
|
||||
}
|
||||
body {
|
||||
font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Microsoft YaHei', sans-serif;
|
||||
@@ -47,594 +44,439 @@
|
||||
min-height: 100vh;
|
||||
}
|
||||
.header {
|
||||
background: linear-gradient(135deg, #0f5ed7 0%, #1677ff 48%, #36a3ff 100%);
|
||||
background: linear-gradient(135deg, #0f5ed7 0%, #1677ff 45%, #36a3ff 100%);
|
||||
color: #fff;
|
||||
padding: 18px 24px;
|
||||
box-shadow: 0 12px 35px rgba(22, 119, 255, 0.2);
|
||||
box-shadow: 0 12px 35px rgba(22,119,255,.2);
|
||||
}
|
||||
.header-content {
|
||||
max-width: 1440px;
|
||||
.header-inner {
|
||||
max-width: 1480px;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.brand h1 {
|
||||
font-size: 24px;
|
||||
font-weight: 800;
|
||||
letter-spacing: 0.02em;
|
||||
.brand h1 { font-size: 26px; font-weight: 800; }
|
||||
.brand p { margin-top: 6px; opacity: .92; font-size: 13px; }
|
||||
.header-actions { display:flex; gap:10px; flex-wrap:wrap; }
|
||||
.chip-btn {
|
||||
border: none; border-radius: 999px; padding: 10px 15px; cursor: pointer; font-weight: 700;
|
||||
background: rgba(255,255,255,.14); color:#fff;
|
||||
}
|
||||
.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); }
|
||||
.chip-btn:hover, .chip-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);
|
||||
max-width: 1480px; margin: 0 auto; padding: 20px;
|
||||
display: grid; grid-template-columns: 320px minmax(0, 1fr) 340px; gap: 18px;
|
||||
}
|
||||
.card {
|
||||
background: var(--card);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius);
|
||||
box-shadow: var(--shadow);
|
||||
backdrop-filter: blur(10px);
|
||||
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;
|
||||
.sidebar, .content, .aside { padding: 20px; }
|
||||
.title-row { display:flex; justify-content:space-between; align-items:center; gap:12px; margin-bottom:14px; }
|
||||
.title-row h2 { font-size: 18px; font-weight: 800; }
|
||||
.muted { color: var(--text-3); }
|
||||
.course-meta {
|
||||
display:grid; grid-template-columns:repeat(2, 1fr); gap:10px; margin-bottom:16px;
|
||||
}
|
||||
.stats-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 10px;
|
||||
margin-bottom: 16px;
|
||||
.meta-box {
|
||||
padding: 14px; border-radius: 16px; border:1px solid var(--border);
|
||||
background: linear-gradient(180deg, var(--primary-soft), rgba(255,255,255,.75));
|
||||
}
|
||||
.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;
|
||||
.meta-box .label { font-size:12px; color:var(--text-3); }
|
||||
.meta-box .value { margin-top:6px; font-size:22px; font-weight:800; }
|
||||
.module-item {
|
||||
border:1px solid var(--border); border-radius:18px; margin-bottom:14px; overflow:hidden;
|
||||
background: rgba(255,255,255,.55);
|
||||
}
|
||||
.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;
|
||||
.module-head {
|
||||
padding: 14px 16px; cursor:pointer; display:flex; justify-content:space-between; gap:12px; align-items:center;
|
||||
background: linear-gradient(90deg, #eef6ff 0%, #f8fbff 100%);
|
||||
color: var(--primary-dark); font-weight: 800;
|
||||
}
|
||||
.progress-fill {
|
||||
height: 100%; background: linear-gradient(90deg, var(--primary), var(--secondary)); transition: width .3s ease;
|
||||
.lesson-list { padding: 12px; }
|
||||
.lesson-item {
|
||||
padding: 12px 14px; border-radius: 14px; cursor:pointer; transition: all .2s ease; margin-bottom:8px;
|
||||
border:1px solid transparent;
|
||||
}
|
||||
.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;
|
||||
.lesson-item:hover { background: var(--primary-soft); }
|
||||
.lesson-item.active {
|
||||
background: linear-gradient(90deg, var(--primary) 0%, var(--accent) 100%); color:#fff;
|
||||
}
|
||||
.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; }
|
||||
.lesson-item .name { font-weight: 700; }
|
||||
.lesson-item .desc { font-size: 12px; margin-top: 5px; opacity: .85; }
|
||||
.hero {
|
||||
padding: 26px; border-radius: 24px;
|
||||
background: linear-gradient(135deg, rgba(22,119,255,.12), rgba(54,163,255,.1));
|
||||
border: 1px solid #d8e7fb;
|
||||
padding: 24px; border-radius: 24px; border:1px solid #d8e7fb;
|
||||
background: linear-gradient(135deg, rgba(22,119,255,.12), rgba(54,163,255,.08));
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
.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; }
|
||||
.hero h2 { font-size: 32px; margin-bottom: 10px; }
|
||||
.hero p { line-height: 1.9; color: var(--text-3); }
|
||||
.hero-actions { display:flex; gap:12px; flex-wrap:wrap; margin-top:16px; }
|
||||
.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;
|
||||
border:none; border-radius:14px; padding: 12px 18px; cursor:pointer; font-weight:800;
|
||||
}
|
||||
.btn-primary { background: linear-gradient(135deg, var(--primary), var(--accent)); color:#fff; }
|
||||
.btn-soft { background: #edf4ff; color: var(--primary-dark); }
|
||||
.lesson-shell { display:none; }
|
||||
.lesson-shell.show { display:block; }
|
||||
.badge-row { display:flex; gap:10px; flex-wrap:wrap; margin-bottom:14px; }
|
||||
.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);
|
||||
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;
|
||||
.lesson-title { font-size: 30px; font-weight: 800; margin-bottom: 10px; }
|
||||
.panel {
|
||||
border:1px solid var(--border); border-radius:18px; padding:18px; margin-bottom:14px;
|
||||
background: linear-gradient(180deg, rgba(255,255,255,.8), rgba(248,251,255,.92));
|
||||
}
|
||||
.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);
|
||||
.panel h3 { font-size:16px; font-weight:800; margin-bottom:12px; color: var(--text-2); }
|
||||
.panel p, .panel li { line-height: 1.9; color: var(--text-2); }
|
||||
.panel ul { padding-left: 18px; }
|
||||
.example-list, .exercise-list { display:flex; flex-direction:column; gap:10px; }
|
||||
.code-block {
|
||||
padding: 12px 14px; border-radius: 14px; background:#0b1220; color:#dbeafe; font-family: Consolas, monospace;
|
||||
overflow:auto;
|
||||
}
|
||||
.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;
|
||||
.exercise-card {
|
||||
border:1px solid var(--border); border-radius:16px; padding:14px; background: rgba(255,255,255,.85);
|
||||
}
|
||||
.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);
|
||||
.exercise-type {
|
||||
display:inline-flex; margin-bottom:8px; padding:4px 8px; border-radius:999px; font-size:12px; font-weight:700;
|
||||
background: #eef4ff; color: var(--primary-dark);
|
||||
}
|
||||
.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-box {
|
||||
margin-top: 12px; border-radius: 18px; overflow:hidden; border:1px solid rgba(59,130,246,.18);
|
||||
background: linear-gradient(180deg, var(--terminal) 0%, var(--terminal-soft) 100%);
|
||||
}
|
||||
.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;
|
||||
.terminal-header { padding: 12px 14px; color:#b9d6ff; font-weight:700; border-bottom:1px solid rgba(148,163,184,.15); }
|
||||
.terminal-input-row { display:flex; gap:10px; padding: 14px; align-items:center; }
|
||||
.prompt { color:#4ade80; font-family: Consolas, monospace; font-weight:700; }
|
||||
.cmd-input {
|
||||
flex:1; border:none; outline:none; background:transparent; color:#fff; font-family: Consolas, monospace; font-size:15px;
|
||||
}
|
||||
.feedback {
|
||||
display: none; padding: 16px 18px; border-radius: 16px; font-size: 14px; font-weight: 600;
|
||||
.terminal-output { padding: 14px; min-height: 120px; white-space: pre-wrap; color:#dbeafe; font-family: Consolas, monospace; }
|
||||
.feedback { display:none; margin-top: 12px; padding: 14px 16px; border-radius: 14px; font-weight:700; }
|
||||
.feedback.show { display:block; }
|
||||
.feedback.success { background: rgba(34,197,94,.1); border:1px solid rgba(34,197,94,.2); color:#15803d; }
|
||||
.feedback.warn { background: rgba(245,158,11,.1); border:1px solid rgba(245,158,11,.2); color:#b45309; }
|
||||
.aside-card {
|
||||
border:1px solid var(--border); border-radius:18px; padding:16px; margin-bottom:14px;
|
||||
background: linear-gradient(180deg, rgba(255,255,255,.88), rgba(247,250,255,.92));
|
||||
}
|
||||
.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);
|
||||
.aside-card h3 { font-size:15px; font-weight:800; margin-bottom:10px; }
|
||||
.aside-card li { margin-left: 18px; line-height: 1.8; color: var(--text-2); }
|
||||
.qa-box {
|
||||
padding:12px; border-radius:14px; background: var(--primary-soft); margin-top:10px; color:var(--text-2);
|
||||
}
|
||||
.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; }
|
||||
.layout { grid-template-columns: 320px minmax(0, 1fr); }
|
||||
.aside { display:none; }
|
||||
}
|
||||
@media (max-width: 820px) {
|
||||
@media (max-width: 860px) {
|
||||
.layout { grid-template-columns: 1fr; }
|
||||
.sidebar { order: 2; }
|
||||
.main-panel { order: 1; }
|
||||
.header-content { flex-direction: column; align-items: start; }
|
||||
.header-inner { flex-direction: column; align-items: flex-start; }
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<header class="header">
|
||||
<div class="header-content">
|
||||
<div class="header-inner">
|
||||
<div class="brand">
|
||||
<h1>🐧 Linux 运维学习平台</h1>
|
||||
<p>内容更系统 · 练习更真实 · 判题更可靠</p>
|
||||
<h1>🐧 Linux 系统学习平台</h1>
|
||||
<p>以知识理解为中心,练习为辅,帮助建立真正可迁移的 Linux 能力。</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>
|
||||
<button class="chip-btn active" onclick="setTheme('light')">浅色</button>
|
||||
<button class="chip-btn" onclick="setTheme('dark')">深色</button>
|
||||
<button class="chip-btn" onclick="resetSandbox()">重置沙盒</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 class="title-row">
|
||||
<h2>课程地图</h2>
|
||||
<span class="muted" id="courseVersion">v4.0</span>
|
||||
</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 class="course-meta">
|
||||
<div class="meta-box"><div class="label">模块数</div><div class="value" id="moduleCount">6</div></div>
|
||||
<div class="meta-box"><div class="label">课时数</div><div class="value" id="lessonCount">18</div></div>
|
||||
<div class="meta-box"><div class="label">练习数</div><div class="value" id="exerciseCount">54</div></div>
|
||||
<div class="meta-box"><div class="label">学习方向</div><div class="value" style="font-size:18px">知识优先</div></div>
|
||||
</div>
|
||||
<div id="courseNav"></div>
|
||||
</aside>
|
||||
|
||||
<main class="main-panel card">
|
||||
<main class="content card">
|
||||
<section class="hero" id="heroPanel">
|
||||
<h2>从命令入门,到运维思维</h2>
|
||||
<h2>先把 Linux 学明白,再去练</h2>
|
||||
<p>
|
||||
这个版本不再只是“输入答案过题”。你现在会得到更真实的沙盒环境、状态可变的文件系统、
|
||||
更合理的后端判题,以及更清晰的学习建议。
|
||||
这个版本不再以“闯关感”作为核心,而是把 Linux 当成一门真正要学懂的技能来组织:
|
||||
每个课时会先说明为什么重要、核心知识点是什么、常见误区在哪里,再给最小示例和少量练习。
|
||||
</p>
|
||||
<div class="hero-actions">
|
||||
<button class="btn btn-primary" onclick="startLearning()">开始第一关</button>
|
||||
<button class="btn btn-secondary" onclick="jumpToFirstUnfinished()">继续上次进度</button>
|
||||
<button class="btn btn-primary" onclick="openFirstLesson()">从第一课开始</button>
|
||||
<button class="btn btn-soft" onclick="openFirstPracticeLesson()">直接看第一组练习</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>
|
||||
<section class="lesson-shell" id="lessonShell">
|
||||
<div class="badge-row">
|
||||
<span class="badge" id="moduleBadge">模块 1</span>
|
||||
<span class="badge" id="commandBadge">命令</span>
|
||||
</div>
|
||||
<div class="lesson-title" id="lessonTitle">课时标题</div>
|
||||
<p class="muted" id="lessonSummary" style="margin-bottom: 18px;"></p>
|
||||
|
||||
<div class="panel">
|
||||
<h3>这一课学什么</h3>
|
||||
<p id="lessonGoal"></p>
|
||||
</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 class="panel">
|
||||
<h3>为什么重要</h3>
|
||||
<p id="lessonWhy"></p>
|
||||
</div>
|
||||
|
||||
<div class="feedback" id="feedbackBox"></div>
|
||||
<div class="panel">
|
||||
<h3>核心知识点</h3>
|
||||
<ul id="conceptList"></ul>
|
||||
</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 class="panel">
|
||||
<h3>最小示例</h3>
|
||||
<div class="example-list" id="exampleList"></div>
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
<h3>常见误区</h3>
|
||||
<ul id="pitfallList"></ul>
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
<h3>典型使用场景</h3>
|
||||
<ul id="scenarioList"></ul>
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
<h3>本课练习</h3>
|
||||
<div class="exercise-list" id="exerciseList"></div>
|
||||
</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>
|
||||
<aside class="aside card">
|
||||
<div class="aside-card">
|
||||
<h3>学习建议</h3>
|
||||
<ul>
|
||||
<li>先理解“命令解决什么问题”,再记参数。</li>
|
||||
<li>不要一开始背太多选项,先掌握最常用组合。</li>
|
||||
<li>日志、配置、目录、进程,是 Linux 学习的四大核心场景。</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="knowledge-card">
|
||||
<div class="small-title">命令讲解</div>
|
||||
<div id="knowledgeBox" class="empty-state">选择任务后,这里会提供命令用途、示例与注意点。</div>
|
||||
<div class="aside-card">
|
||||
<h3>当前课时提示</h3>
|
||||
<div id="asideHint" class="muted">打开课程后,这里会显示与当前课时相关的补充提醒。</div>
|
||||
</div>
|
||||
<div class="milestone-card">
|
||||
<div class="small-title">当前目标</div>
|
||||
<div id="milestoneBox" class="empty-state">开始第一关,逐步建立 Linux 运维基本功。</div>
|
||||
<div class="aside-card">
|
||||
<h3>理解型问题</h3>
|
||||
<div id="qaBox" class="muted">选择课时后,这里会显示理解题和场景题,帮助你形成真正的命令认识。</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 进程名 是经典组合。'
|
||||
}
|
||||
};
|
||||
let COURSE = null;
|
||||
let currentLesson = null;
|
||||
let currentTheme = localStorage.getItem('linux_course_theme') || 'light';
|
||||
|
||||
document.addEventListener('DOMContentLoaded', async () => {
|
||||
applyTheme(currentTheme);
|
||||
setMode(currentMode, false);
|
||||
await loadCourseData();
|
||||
setTheme(currentTheme, false);
|
||||
await loadCourse();
|
||||
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;
|
||||
async function loadCourse() {
|
||||
const res = await fetch('/api/course');
|
||||
COURSE = await res.json();
|
||||
document.getElementById('courseVersion').textContent = 'v' + (COURSE.meta.version || '4.0');
|
||||
document.getElementById('moduleCount').textContent = COURSE.meta.module_count || COURSE.modules.length;
|
||||
document.getElementById('lessonCount').textContent = COURSE.meta.total_lessons || getAllLessons().length;
|
||||
document.getElementById('exerciseCount').textContent = COURSE.meta.total_exercises || getAllExercises().length;
|
||||
}
|
||||
|
||||
function getAllTasks() {
|
||||
return COURSE_DATA ? COURSE_DATA.levels.flatMap(level => level.challenges.map(task => ({...task, levelTitle: level.title, levelId: level.id}))) : [];
|
||||
function getAllLessons() {
|
||||
return COURSE.modules.flatMap(module => module.lessons.map(lesson => ({ ...lesson, moduleId: module.id, moduleTitle: module.title, moduleSummary: module.summary })));
|
||||
}
|
||||
|
||||
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 getAllExercises() {
|
||||
return getAllLessons().flatMap(lesson => lesson.exercises.map(ex => ({ ...ex, lessonTitle: lesson.title, moduleTitle: lesson.moduleTitle })));
|
||||
}
|
||||
|
||||
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);
|
||||
nav.innerHTML = COURSE.modules.map((module, index) => {
|
||||
const lessonHtml = module.lessons.map(lesson => {
|
||||
const active = currentLesson && currentLesson.id === lesson.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>`;
|
||||
<div class="lesson-item ${active ? 'active' : ''}" onclick="openLesson('${module.id}', '${lesson.id}')">
|
||||
<div class="name">${lesson.title}</div>
|
||||
<div class="desc">${lesson.goal}</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
return `
|
||||
<div style="margin-bottom:12px;">
|
||||
<div class="level-header" onclick="toggleLevel(${levelIndex})">
|
||||
<span>${level.title}</span>
|
||||
<span>共 ${level.challenges.length} 题</span>
|
||||
<div class="module-item">
|
||||
<div class="module-head" onclick="toggleModule(${index})">
|
||||
<span>${module.title}</span>
|
||||
<span>${module.lessons.length} 课</span>
|
||||
</div>
|
||||
<ul class="task-list" id="level-${levelIndex}">${items}</ul>
|
||||
<div class="lesson-list" id="module-${index}">${lessonHtml}</div>
|
||||
</div>`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function toggleLevel(index) {
|
||||
const el = document.getElementById(`level-${index}`);
|
||||
el.style.display = el.style.display === 'none' ? 'block' : 'none';
|
||||
function toggleModule(index) {
|
||||
const el = document.getElementById(`module-${index}`);
|
||||
el.style.display = el.style.display === 'none' ? 'block' : 'block';
|
||||
}
|
||||
|
||||
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);
|
||||
function openLesson(moduleId, lessonId) {
|
||||
const module = COURSE.modules.find(m => m.id === moduleId);
|
||||
const lesson = module.lessons.find(l => l.id === lessonId);
|
||||
currentLesson = { ...lesson, moduleTitle: module.title, moduleSummary: module.summary };
|
||||
renderCourseNav();
|
||||
renderLesson(currentLesson);
|
||||
}
|
||||
|
||||
function renderTask(task) {
|
||||
function renderLesson(lesson) {
|
||||
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);
|
||||
document.getElementById('lessonShell').classList.add('show');
|
||||
document.getElementById('moduleBadge').textContent = lesson.moduleTitle;
|
||||
document.getElementById('commandBadge').textContent = lesson.command || '综合命令';
|
||||
document.getElementById('lessonTitle').textContent = lesson.title;
|
||||
document.getElementById('lessonSummary').textContent = lesson.moduleSummary || '';
|
||||
document.getElementById('lessonGoal').textContent = lesson.goal || '';
|
||||
document.getElementById('lessonWhy').textContent = lesson.why_it_matters || '';
|
||||
renderList('conceptList', lesson.concepts || []);
|
||||
renderList('pitfallList', lesson.pitfalls || []);
|
||||
renderList('scenarioList', lesson.scenarios || []);
|
||||
document.getElementById('exampleList').innerHTML = (lesson.examples || []).map(ex => `<div class="code-block">${escapeHtml(ex)}</div>`).join('');
|
||||
document.getElementById('exerciseList').innerHTML = (lesson.exercises || []).map(ex => renderExercise(ex)).join('');
|
||||
document.getElementById('asideHint').textContent = (lesson.pitfalls || [])[0] || '这一课没有额外提示。';
|
||||
document.getElementById('qaBox').innerHTML = buildQaBox(lesson.exercises || []);
|
||||
}
|
||||
|
||||
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>
|
||||
function renderList(targetId, list) {
|
||||
const el = document.getElementById(targetId);
|
||||
el.innerHTML = list.map(item => `<li>${escapeHtml(item)}</li>`).join('');
|
||||
}
|
||||
|
||||
function renderExercise(ex) {
|
||||
const typeLabel = ex.type === 'operation' ? '操作练习' : ex.type === 'understanding' ? '理解题' : '场景题';
|
||||
const body = ex.type === 'operation'
|
||||
? `
|
||||
<div style="font-weight:700; margin-bottom:8px;">${ex.title || '练习'}</div>
|
||||
<div class="muted" style="line-height:1.8; margin-bottom:10px;">${ex.hint || '请完成对应命令。'}</div>
|
||||
<div class="terminal-box">
|
||||
<div class="terminal-header">终端练习区</div>
|
||||
<div class="terminal-input-row">
|
||||
<span class="prompt">$</span>
|
||||
<input class="cmd-input" id="input-${ex.id}" placeholder="输入命令,例如 ${ex.solution ? ex.solution[0] : 'pwd'}" onkeypress="if(event.key==='Enter') runExercise('${ex.id}')" />
|
||||
<button class="btn btn-primary" onclick="runExercise('${ex.id}')">执行</button>
|
||||
</div>
|
||||
<div class="terminal-output" id="output-${ex.id}">等待输入命令...</div>
|
||||
</div>
|
||||
<div class="feedback" id="feedback-${ex.id}"></div>
|
||||
`
|
||||
: `
|
||||
<div style="font-weight:700; margin-bottom:8px;">${ex.question || '理解题'}</div>
|
||||
<div class="qa-box">参考答案方向:${escapeHtml(ex.answer || '请结合本课内容自行总结')}</div>
|
||||
`;
|
||||
} else {
|
||||
box.innerHTML = '<div class="empty-state">这题更偏向综合操作。先理解目标,再尝试一步步拆分命令。</div>';
|
||||
}
|
||||
return `
|
||||
<div class="exercise-card">
|
||||
<div class="exercise-type">${typeLabel}</div>
|
||||
${body}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
async function executeCommand() {
|
||||
const input = document.getElementById('cmdInput');
|
||||
function buildQaBox(exercises) {
|
||||
const textItems = exercises.filter(ex => ex.type !== 'operation');
|
||||
if (!textItems.length) return '本课暂无理解型问题。';
|
||||
return textItems.map(ex => `<div class="qa-box"><strong>${escapeHtml(ex.question || '问题')}</strong><br/>${escapeHtml(ex.answer || '')}</div>`).join('');
|
||||
}
|
||||
|
||||
async function runExercise(exerciseId) {
|
||||
const input = document.getElementById(`input-${exerciseId}`);
|
||||
const output = document.getElementById(`output-${exerciseId}`);
|
||||
const feedback = document.getElementById(`feedback-${exerciseId}`);
|
||||
const cmd = input.value.trim();
|
||||
if (!cmd || !currentTask) return;
|
||||
const outputEl = document.getElementById('cmdOutput');
|
||||
outputEl.textContent = `$ ${cmd}\n\n执行中...`;
|
||||
hideFeedback();
|
||||
if (!cmd) return;
|
||||
output.textContent = `$ ${cmd}\n\n执行中...`;
|
||||
feedback.className = 'feedback';
|
||||
feedback.textContent = '';
|
||||
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 || '');
|
||||
const runRes = await fetch('/api/run?cmd=' + encodeURIComponent(cmd));
|
||||
const runData = await runRes.json();
|
||||
output.textContent = `$ ${cmd}\n\n${runData.output || runData.message || '(无输出)'}`;
|
||||
const checkRes = await fetch(`/api/check?exercise_id=${encodeURIComponent(exerciseId)}&last_cmd=${encodeURIComponent(cmd)}&output=${encodeURIComponent(runData.output || '')}`);
|
||||
const checkData = await checkRes.json();
|
||||
feedback.className = `feedback show ${checkData.success ? 'success' : 'warn'}`;
|
||||
feedback.textContent = checkData.message + (checkData.next_suggestion ? `\n${checkData.next_suggestion}` : '');
|
||||
} catch (e) {
|
||||
outputEl.textContent = `❌ 执行失败:${e.message}`;
|
||||
showFeedback('error', '命令执行失败,请稍后重试。');
|
||||
output.textContent = `❌ 执行失败:${e.message}`;
|
||||
feedback.className = 'feedback show warn';
|
||||
feedback.textContent = '执行失败,请稍后重试。';
|
||||
}
|
||||
}
|
||||
|
||||
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', '沙盒环境已重置,目录、文件和权限状态都恢复到初始值。');
|
||||
alert('沙盒环境已重置。');
|
||||
}
|
||||
|
||||
function showFeedback(type, text) {
|
||||
const box = document.getElementById('feedbackBox');
|
||||
box.className = `feedback show ${type}`;
|
||||
box.textContent = text;
|
||||
function setTheme(theme, save = true) {
|
||||
currentTheme = theme;
|
||||
document.documentElement.setAttribute('data-theme', theme);
|
||||
if (save) localStorage.setItem('linux_course_theme', theme);
|
||||
}
|
||||
|
||||
function hideFeedback() {
|
||||
const box = document.getElementById('feedbackBox');
|
||||
box.className = 'feedback';
|
||||
box.textContent = '';
|
||||
function openFirstLesson() {
|
||||
const firstModule = COURSE.modules[0];
|
||||
const firstLesson = firstModule.lessons[0];
|
||||
openLesson(firstModule.id, firstLesson.id);
|
||||
}
|
||||
|
||||
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;
|
||||
function openFirstPracticeLesson() {
|
||||
for (const module of COURSE.modules) {
|
||||
for (const lesson of module.lessons) {
|
||||
if ((lesson.exercises || []).some(ex => ex.type === 'operation')) {
|
||||
openLesson(module.id, lesson.id);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function escapeHtml(str) {
|
||||
return String(str)
|
||||
.replaceAll('&', '&')
|
||||
.replaceAll('<', '<')
|
||||
.replaceAll('>', '>')
|
||||
.replaceAll('"', '"')
|
||||
.replaceAll("'", ''');
|
||||
}
|
||||
|
||||
window.toggleModule = toggleModule;
|
||||
window.openLesson = openLesson;
|
||||
window.runExercise = runExercise;
|
||||
window.resetSandbox = resetSandbox;
|
||||
window.jumpToFirstUnfinished = jumpToFirstUnfinished;
|
||||
window.setTheme = setTheme;
|
||||
window.openFirstLesson = openFirstLesson;
|
||||
window.openFirstPracticeLesson = openFirstPracticeLesson;
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user