Files
linux-practice/index.html

483 lines
21 KiB
HTML

<!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;
--accent: #36a3ff;
--success: #22c55e;
--warning: #f59e0b;
--danger: #ef4444;
--bg: #f5f9ff;
--bg-2: #edf4ff;
--card: rgba(255,255,255,0.95);
--text: #0f172a;
--text-2: #334155;
--text-3: #64748b;
--border: #dbe7f5;
--shadow: 0 18px 45px rgba(15, 94, 215, 0.12);
--radius: 22px;
--terminal: #0b1220;
--terminal-soft: #101a2f;
}
[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;
--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;
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 45%, #36a3ff 100%);
color: #fff;
padding: 18px 24px;
box-shadow: 0 12px 35px rgba(22,119,255,.2);
}
.header-inner {
max-width: 1480px;
margin: 0 auto;
display: flex;
justify-content: space-between;
gap: 20px;
align-items: center;
flex-wrap: wrap;
}
.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;
}
.chip-btn:hover, .chip-btn.active { background:#fff; color:var(--primary); }
.layout {
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);
}
.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;
}
.meta-box {
padding: 14px; border-radius: 16px; border:1px solid var(--border);
background: linear-gradient(180deg, var(--primary-soft), rgba(255,255,255,.75));
}
.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);
}
.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;
}
.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;
}
.lesson-item:hover { background: var(--primary-soft); }
.lesson-item.active {
background: linear-gradient(90deg, var(--primary) 0%, var(--accent) 100%); color:#fff;
}
.lesson-item .name { font-weight: 700; }
.lesson-item .desc { font-size: 12px; margin-top: 5px; opacity: .85; }
.hero {
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: 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-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);
}
.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));
}
.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;
}
.exercise-card {
border:1px solid var(--border); border-radius:16px; padding:14px; background: rgba(255,255,255,.85);
}
.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);
}
.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-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;
}
.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));
}
.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);
}
@media (max-width: 1240px) {
.layout { grid-template-columns: 320px minmax(0, 1fr); }
.aside { display:none; }
}
@media (max-width: 860px) {
.layout { grid-template-columns: 1fr; }
.header-inner { flex-direction: column; align-items: flex-start; }
}
</style>
</head>
<body>
<header class="header">
<div class="header-inner">
<div class="brand">
<h1>🐧 Linux 运维全场景学习平台</h1>
<p>以知识理解为中心,尽量覆盖运维强相关全场景,帮助建立真正可迁移的 Linux 能力。</p>
</div>
<div class="header-actions">
<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="title-row">
<h2>课程地图</h2>
<span class="muted" id="courseVersion">v4.0</span>
</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="content card">
<section class="hero" id="heroPanel">
<h2>先建立运维视角,再去学命令</h2>
<p>
这个版本不再把 Linux 做成“命令闯关页”,而是尽量按运维全场景组织课程:
从文件系统、日志、资源、服务、网络、权限,到包管理、自动化、综合排障,逐步建立完整认知。
</p>
<div class="hero-actions">
<button class="btn btn-primary" onclick="openFirstLesson()">从第一课开始</button>
<button class="btn btn-soft" onclick="openFirstPracticeLesson()">直接看第一组练习</button>
</div>
</section>
<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="panel">
<h3>为什么重要</h3>
<p id="lessonWhy"></p>
</div>
<div class="panel">
<h3>核心知识点</h3>
<ul id="conceptList"></ul>
</div>
<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="aside card">
<div class="aside-card">
<h3>学习建议</h3>
<ul>
<li>先理解“场景里为什么需要这个命令”,再记参数。</li>
<li>不要只学单个命令,要学命令之间如何串成排障链路。</li>
<li>目录、日志、资源、服务、网络、权限,是 Linux 运维学习的六大核心场景。</li>
</ul>
</div>
<div class="aside-card">
<h3>当前课时提示</h3>
<div id="asideHint" class="muted">打开课程后,这里会显示与当前课时相关的补充提醒。</div>
</div>
<div class="aside-card">
<h3>理解型问题</h3>
<div id="qaBox" class="muted">选择课时后,这里会显示理解题和场景题,帮助你形成真正的命令认识。</div>
</div>
</aside>
</div>
<script>
let COURSE = null;
let currentLesson = null;
let currentTheme = localStorage.getItem('linux_course_theme') || 'light';
document.addEventListener('DOMContentLoaded', async () => {
setTheme(currentTheme, false);
await loadCourse();
renderCourseNav();
});
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 getAllLessons() {
return COURSE.modules.flatMap(module => module.lessons.map(lesson => ({ ...lesson, moduleId: module.id, moduleTitle: module.title, moduleSummary: module.summary })));
}
function getAllExercises() {
return getAllLessons().flatMap(lesson => lesson.exercises.map(ex => ({ ...ex, lessonTitle: lesson.title, moduleTitle: lesson.moduleTitle })));
}
function renderCourseNav() {
const nav = document.getElementById('courseNav');
nav.innerHTML = COURSE.modules.map((module, index) => {
const lessonHtml = module.lessons.map(lesson => {
const active = currentLesson && currentLesson.id === lesson.id;
return `
<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 class="module-item">
<div class="module-head" onclick="toggleModule(${index})">
<span>${module.title}</span>
<span>${module.lessons.length} 课</span>
</div>
<div class="lesson-list" id="module-${index}">${lessonHtml}</div>
</div>`;
}).join('');
}
function toggleModule(index) {
const el = document.getElementById(`module-${index}`);
el.style.display = el.style.display === 'none' ? 'block' : 'block';
}
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 renderLesson(lesson) {
document.getElementById('heroPanel').style.display = 'none';
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 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>
`;
return `
<div class="exercise-card">
<div class="exercise-type">${typeLabel}</div>
${body}
</div>`;
}
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) return;
output.textContent = `$ ${cmd}\n\n执行中...`;
feedback.className = 'feedback';
feedback.textContent = '';
try {
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) {
output.textContent = `❌ 执行失败:${e.message}`;
feedback.className = 'feedback show warn';
feedback.textContent = '执行失败,请稍后重试。';
}
}
async function resetSandbox() {
await fetch('/api/reset', { method: 'POST' });
alert('沙盒环境已重置。');
}
function setTheme(theme, save = true) {
currentTheme = theme;
document.documentElement.setAttribute('data-theme', theme);
if (save) localStorage.setItem('linux_course_theme', theme);
}
function openFirstLesson() {
const firstModule = COURSE.modules[0];
const firstLesson = firstModule.lessons[0];
openLesson(firstModule.id, firstLesson.id);
}
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('&', '&amp;')
.replaceAll('<', '&lt;')
.replaceAll('>', '&gt;')
.replaceAll('"', '&quot;')
.replaceAll("'", '&#39;');
}
window.toggleModule = toggleModule;
window.openLesson = openLesson;
window.runExercise = runExercise;
window.resetSandbox = resetSandbox;
window.setTheme = setTheme;
window.openFirstLesson = openFirstLesson;
window.openFirstPracticeLesson = openFirstPracticeLesson;
</script>
</body>
</html>