927 lines
32 KiB
HTML
927 lines
32 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: #4CAF50;
|
|||
|
|
--primary-dark: #388E3C;
|
|||
|
|
--secondary: #2196F3;
|
|||
|
|
--accent: #FF9800;
|
|||
|
|
--bg: #f5f7fa;
|
|||
|
|
--card-bg: #ffffff;
|
|||
|
|
--text: #333333;
|
|||
|
|
--text-light: #666666;
|
|||
|
|
--border: #e0e0e0;
|
|||
|
|
--success: #4CAF50;
|
|||
|
|
--error: #f44336;
|
|||
|
|
--warning: #ff9800;
|
|||
|
|
}
|
|||
|
|
[data-theme="dark"] {
|
|||
|
|
--bg: #1a1a2e;
|
|||
|
|
--card-bg: #16213e;
|
|||
|
|
--text: #eaeaea;
|
|||
|
|
--text-light: #a0a0a0;
|
|||
|
|
--border: #0f3460;
|
|||
|
|
}
|
|||
|
|
body {
|
|||
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Microsoft YaHei', sans-serif;
|
|||
|
|
background: var(--bg);
|
|||
|
|
color: var(--text);
|
|||
|
|
line-height: 1.6;
|
|||
|
|
}
|
|||
|
|
/* Header */
|
|||
|
|
.header {
|
|||
|
|
background: linear-gradient(135deg, var(--primary) 0%, var(--primary-dark) 100%);
|
|||
|
|
color: white;
|
|||
|
|
padding: 1rem 2rem;
|
|||
|
|
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
|||
|
|
}
|
|||
|
|
.header-content {
|
|||
|
|
max-width: 1400px;
|
|||
|
|
margin: 0 auto;
|
|||
|
|
display: flex;
|
|||
|
|
justify-content: space-between;
|
|||
|
|
align-items: center;
|
|||
|
|
}
|
|||
|
|
.logo {
|
|||
|
|
font-size: 1.5rem;
|
|||
|
|
font-weight: bold;
|
|||
|
|
}
|
|||
|
|
.header-actions {
|
|||
|
|
display: flex;
|
|||
|
|
gap: 1rem;
|
|||
|
|
align-items: center;
|
|||
|
|
}
|
|||
|
|
.mode-toggle {
|
|||
|
|
background: rgba(255,255,255,0.2);
|
|||
|
|
border: none;
|
|||
|
|
color: white;
|
|||
|
|
padding: 0.5rem 1rem;
|
|||
|
|
border-radius: 20px;
|
|||
|
|
cursor: pointer;
|
|||
|
|
transition: all 0.3s;
|
|||
|
|
}
|
|||
|
|
.mode-toggle:hover {
|
|||
|
|
background: rgba(255,255,255,0.3);
|
|||
|
|
}
|
|||
|
|
.mode-toggle.active {
|
|||
|
|
background: white;
|
|||
|
|
color: var(--primary);
|
|||
|
|
}
|
|||
|
|
/* Main Layout */
|
|||
|
|
.container {
|
|||
|
|
max-width: 1400px;
|
|||
|
|
margin: 0 auto;
|
|||
|
|
display: grid;
|
|||
|
|
grid-template-columns: 280px 1fr 350px;
|
|||
|
|
gap: 1.5rem;
|
|||
|
|
padding: 1.5rem;
|
|||
|
|
min-height: calc(100vh - 70px);
|
|||
|
|
}
|
|||
|
|
/* Sidebar */
|
|||
|
|
.sidebar {
|
|||
|
|
background: var(--card-bg);
|
|||
|
|
border-radius: 12px;
|
|||
|
|
padding: 1rem;
|
|||
|
|
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
|
|||
|
|
height: fit-content;
|
|||
|
|
}
|
|||
|
|
.sidebar-title {
|
|||
|
|
font-size: 1.1rem;
|
|||
|
|
font-weight: 600;
|
|||
|
|
margin-bottom: 1rem;
|
|||
|
|
padding-bottom: 0.5rem;
|
|||
|
|
border-bottom: 2px solid var(--primary);
|
|||
|
|
}
|
|||
|
|
.level-section {
|
|||
|
|
margin-bottom: 1rem;
|
|||
|
|
}
|
|||
|
|
.level-header {
|
|||
|
|
font-weight: 600;
|
|||
|
|
color: var(--primary);
|
|||
|
|
padding: 0.5rem;
|
|||
|
|
background: rgba(76,175,80,0.1);
|
|||
|
|
border-radius: 6px;
|
|||
|
|
margin-bottom: 0.5rem;
|
|||
|
|
cursor: pointer;
|
|||
|
|
}
|
|||
|
|
.task-list {
|
|||
|
|
list-style: none;
|
|||
|
|
}
|
|||
|
|
.task-item {
|
|||
|
|
padding: 0.6rem 0.8rem;
|
|||
|
|
margin: 0.3rem 0;
|
|||
|
|
border-radius: 6px;
|
|||
|
|
cursor: pointer;
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
gap: 0.5rem;
|
|||
|
|
transition: all 0.2s;
|
|||
|
|
font-size: 0.9rem;
|
|||
|
|
}
|
|||
|
|
.task-item:hover {
|
|||
|
|
background: var(--bg);
|
|||
|
|
}
|
|||
|
|
.task-item.active {
|
|||
|
|
background: var(--primary);
|
|||
|
|
color: white;
|
|||
|
|
}
|
|||
|
|
.task-item.completed {
|
|||
|
|
opacity: 0.7;
|
|||
|
|
}
|
|||
|
|
.task-status {
|
|||
|
|
font-size: 1rem;
|
|||
|
|
}
|
|||
|
|
/* Main Content */
|
|||
|
|
.main-content {
|
|||
|
|
background: var(--card-bg);
|
|||
|
|
border-radius: 12px;
|
|||
|
|
padding: 2rem;
|
|||
|
|
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
|
|||
|
|
}
|
|||
|
|
.welcome-panel {
|
|||
|
|
text-align: center;
|
|||
|
|
padding: 3rem 2rem;
|
|||
|
|
}
|
|||
|
|
.welcome-title {
|
|||
|
|
font-size: 2rem;
|
|||
|
|
margin-bottom: 1rem;
|
|||
|
|
background: linear-gradient(135deg, var(--primary), var(--secondary));
|
|||
|
|
-webkit-background-clip: text;
|
|||
|
|
-webkit-text-fill-color: transparent;
|
|||
|
|
}
|
|||
|
|
.welcome-desc {
|
|||
|
|
color: var(--text-light);
|
|||
|
|
font-size: 1.1rem;
|
|||
|
|
margin-bottom: 2rem;
|
|||
|
|
}
|
|||
|
|
.start-btn {
|
|||
|
|
background: linear-gradient(135deg, var(--primary) 0%, var(--primary-dark) 100%);
|
|||
|
|
color: white;
|
|||
|
|
border: none;
|
|||
|
|
padding: 1rem 3rem;
|
|||
|
|
font-size: 1.1rem;
|
|||
|
|
border-radius: 30px;
|
|||
|
|
cursor: pointer;
|
|||
|
|
transition: transform 0.2s, box-shadow 0.2s;
|
|||
|
|
}
|
|||
|
|
.start-btn:hover {
|
|||
|
|
transform: translateY(-2px);
|
|||
|
|
box-shadow: 0 5px 20px rgba(76,175,80,0.4);
|
|||
|
|
}
|
|||
|
|
/* Task Panel */
|
|||
|
|
.task-panel {
|
|||
|
|
display: none;
|
|||
|
|
}
|
|||
|
|
.task-header {
|
|||
|
|
margin-bottom: 1.5rem;
|
|||
|
|
}
|
|||
|
|
.task-title {
|
|||
|
|
font-size: 1.5rem;
|
|||
|
|
margin-bottom: 0.5rem;
|
|||
|
|
}
|
|||
|
|
.task-meta {
|
|||
|
|
display: flex;
|
|||
|
|
gap: 1rem;
|
|||
|
|
color: var(--text-light);
|
|||
|
|
font-size: 0.9rem;
|
|||
|
|
}
|
|||
|
|
.task-description {
|
|||
|
|
background: linear-gradient(135deg, #f8f9fa 0%, #e9ecef 100%);
|
|||
|
|
padding: 1.5rem;
|
|||
|
|
border-radius: 10px;
|
|||
|
|
margin-bottom: 1.5rem;
|
|||
|
|
border-left: 4px solid var(--primary);
|
|||
|
|
}
|
|||
|
|
.task-description h3 {
|
|||
|
|
color: var(--primary);
|
|||
|
|
margin-bottom: 0.8rem;
|
|||
|
|
}
|
|||
|
|
.task-description code {
|
|||
|
|
background: rgba(76,175,80,0.1);
|
|||
|
|
padding: 0.2rem 0.5rem;
|
|||
|
|
border-radius: 4px;
|
|||
|
|
color: var(--primary-dark);
|
|||
|
|
font-family: 'Consolas', monospace;
|
|||
|
|
}
|
|||
|
|
/* Command Input Area */
|
|||
|
|
.command-area {
|
|||
|
|
background: #1e1e1e;
|
|||
|
|
border-radius: 10px;
|
|||
|
|
padding: 1rem;
|
|||
|
|
margin-bottom: 1rem;
|
|||
|
|
}
|
|||
|
|
.command-input-wrapper {
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
gap: 0.5rem;
|
|||
|
|
margin-bottom: 0.5rem;
|
|||
|
|
}
|
|||
|
|
.prompt {
|
|||
|
|
color: #4CAF50;
|
|||
|
|
font-family: 'Consolas', monospace;
|
|||
|
|
font-weight: bold;
|
|||
|
|
}
|
|||
|
|
.command-input {
|
|||
|
|
flex: 1;
|
|||
|
|
background: transparent;
|
|||
|
|
border: none;
|
|||
|
|
color: #fff;
|
|||
|
|
font-family: 'Consolas', monospace;
|
|||
|
|
font-size: 1rem;
|
|||
|
|
outline: none;
|
|||
|
|
}
|
|||
|
|
.command-output {
|
|||
|
|
background: #2d2d2d;
|
|||
|
|
padding: 1rem;
|
|||
|
|
border-radius: 6px;
|
|||
|
|
min-height: 100px;
|
|||
|
|
max-height: 300px;
|
|||
|
|
overflow-y: auto;
|
|||
|
|
font-family: 'Consolas', monospace;
|
|||
|
|
color: #ddd;
|
|||
|
|
white-space: pre-wrap;
|
|||
|
|
margin-top: 0.5rem;
|
|||
|
|
}
|
|||
|
|
.action-buttons {
|
|||
|
|
display: flex;
|
|||
|
|
gap: 1rem;
|
|||
|
|
margin-top: 1rem;
|
|||
|
|
}
|
|||
|
|
.btn {
|
|||
|
|
padding: 0.8rem 1.5rem;
|
|||
|
|
border: none;
|
|||
|
|
border-radius: 6px;
|
|||
|
|
cursor: pointer;
|
|||
|
|
font-size: 1rem;
|
|||
|
|
transition: all 0.2s;
|
|||
|
|
}
|
|||
|
|
.btn-primary {
|
|||
|
|
background: var(--primary);
|
|||
|
|
color: white;
|
|||
|
|
}
|
|||
|
|
.btn-primary:hover {
|
|||
|
|
background: var(--primary-dark);
|
|||
|
|
}
|
|||
|
|
.btn-secondary {
|
|||
|
|
background: var(--border);
|
|||
|
|
color: var(--text);
|
|||
|
|
}
|
|||
|
|
.btn-hint {
|
|||
|
|
background: var(--accent);
|
|||
|
|
color: white;
|
|||
|
|
}
|
|||
|
|
/* Learning Panel (Right Sidebar) */
|
|||
|
|
.learning-panel {
|
|||
|
|
background: var(--card-bg);
|
|||
|
|
border-radius: 12px;
|
|||
|
|
padding: 1.5rem;
|
|||
|
|
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
|
|||
|
|
height: fit-content;
|
|||
|
|
}
|
|||
|
|
.panel-title {
|
|||
|
|
font-size: 1.1rem;
|
|||
|
|
font-weight: 600;
|
|||
|
|
margin-bottom: 1rem;
|
|||
|
|
display: flex;
|
|||
|
|
align-items: center;
|
|||
|
|
gap: 0.5rem;
|
|||
|
|
}
|
|||
|
|
.panel-title::before {
|
|||
|
|
content: "📚";
|
|||
|
|
}
|
|||
|
|
.command-detail {
|
|||
|
|
display: none;
|
|||
|
|
}
|
|||
|
|
.command-detail.active {
|
|||
|
|
display: block;
|
|||
|
|
}
|
|||
|
|
.cmd-name {
|
|||
|
|
font-size: 1.3rem;
|
|||
|
|
font-weight: bold;
|
|||
|
|
color: var(--primary);
|
|||
|
|
margin-bottom: 0.5rem;
|
|||
|
|
font-family: 'Consolas', monospace;
|
|||
|
|
}
|
|||
|
|
.cmd-desc {
|
|||
|
|
color: var(--text-light);
|
|||
|
|
margin-bottom: 1rem;
|
|||
|
|
line-height: 1.8;
|
|||
|
|
}
|
|||
|
|
.cmd-section {
|
|||
|
|
margin-bottom: 1.2rem;
|
|||
|
|
}
|
|||
|
|
.cmd-section-title {
|
|||
|
|
font-weight: 600;
|
|||
|
|
color: var(--secondary);
|
|||
|
|
margin-bottom: 0.5rem;
|
|||
|
|
font-size: 0.95rem;
|
|||
|
|
}
|
|||
|
|
.param-list {
|
|||
|
|
list-style: none;
|
|||
|
|
}
|
|||
|
|
.param-item {
|
|||
|
|
padding: 0.5rem;
|
|||
|
|
background: var(--bg);
|
|||
|
|
border-radius: 6px;
|
|||
|
|
margin-bottom: 0.5rem;
|
|||
|
|
font-size: 0.9rem;
|
|||
|
|
}
|
|||
|
|
.param-name {
|
|||
|
|
font-family: 'Consolas', monospace;
|
|||
|
|
color: var(--accent);
|
|||
|
|
font-weight: 600;
|
|||
|
|
}
|
|||
|
|
.example-box {
|
|||
|
|
background: #f8f9fa;
|
|||
|
|
border-left: 3px solid var(--secondary);
|
|||
|
|
padding: 0.8rem;
|
|||
|
|
border-radius: 0 6px 6px 0;
|
|||
|
|
margin: 0.5rem 0;
|
|||
|
|
}
|
|||
|
|
.example-box code {
|
|||
|
|
font-family: 'Consolas', monospace;
|
|||
|
|
color: var(--primary-dark);
|
|||
|
|
}
|
|||
|
|
/* Success Panel */
|
|||
|
|
.success-panel {
|
|||
|
|
display: none;
|
|||
|
|
background: linear-gradient(135deg, #d4edda 0%, #c3e6cb 100%);
|
|||
|
|
border: 1px solid var(--success);
|
|||
|
|
border-radius: 10px;
|
|||
|
|
padding: 1.5rem;
|
|||
|
|
margin-top: 1rem;
|
|||
|
|
}
|
|||
|
|
.success-panel.show {
|
|||
|
|
display: block;
|
|||
|
|
}
|
|||
|
|
.success-title {
|
|||
|
|
color: var(--success);
|
|||
|
|
font-size: 1.2rem;
|
|||
|
|
font-weight: 600;
|
|||
|
|
margin-bottom: 0.5rem;
|
|||
|
|
}
|
|||
|
|
.deep-learning-btn {
|
|||
|
|
background: var(--secondary);
|
|||
|
|
color: white;
|
|||
|
|
border: none;
|
|||
|
|
padding: 0.8rem 1.5rem;
|
|||
|
|
border-radius: 6px;
|
|||
|
|
cursor: pointer;
|
|||
|
|
margin-top: 1rem;
|
|||
|
|
}
|
|||
|
|
/* Progress Bar */
|
|||
|
|
.progress-section {
|
|||
|
|
margin-bottom: 1.5rem;
|
|||
|
|
}
|
|||
|
|
.progress-bar {
|
|||
|
|
height: 8px;
|
|||
|
|
background: var(--border);
|
|||
|
|
border-radius: 4px;
|
|||
|
|
overflow: hidden;
|
|||
|
|
}
|
|||
|
|
.progress-fill {
|
|||
|
|
height: 100%;
|
|||
|
|
background: linear-gradient(90deg, var(--primary), var(--secondary));
|
|||
|
|
transition: width 0.3s;
|
|||
|
|
}
|
|||
|
|
.progress-text {
|
|||
|
|
text-align: center;
|
|||
|
|
margin-top: 0.5rem;
|
|||
|
|
color: var(--text-light);
|
|||
|
|
font-size: 0.9rem;
|
|||
|
|
}
|
|||
|
|
/* Responsive */
|
|||
|
|
@media (max-width: 1200px) {
|
|||
|
|
.container {
|
|||
|
|
grid-template-columns: 260px 1fr;
|
|||
|
|
}
|
|||
|
|
.learning-panel {
|
|||
|
|
display: none;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
@media (max-width: 768px) {
|
|||
|
|
.container {
|
|||
|
|
grid-template-columns: 1fr;
|
|||
|
|
}
|
|||
|
|
.sidebar {
|
|||
|
|
display: none;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
</style>
|
|||
|
|
</head>
|
|||
|
|
<body>
|
|||
|
|
<header class="header">
|
|||
|
|
<div class="header-content">
|
|||
|
|
<div class="logo">🐧 Linux 学习平台</div>
|
|||
|
|
<div class="header-actions">
|
|||
|
|
<button class="mode-toggle active" id="learnMode" onclick="setMode('learn')">📖 学习模式</button>
|
|||
|
|
<button class="mode-toggle" id="practiceMode" onclick="setMode('practice')">✏️ 练习模式</button>
|
|||
|
|
<button class="mode-toggle" onclick="toggleTheme()">🌓</button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</header>
|
|||
|
|
|
|||
|
|
<div class="container">
|
|||
|
|
<!-- Left Sidebar: Course Navigation -->
|
|||
|
|
<aside class="sidebar">
|
|||
|
|
<div class="sidebar-title">📚 课程目录</div>
|
|||
|
|
<div class="progress-section">
|
|||
|
|
<div class="progress-bar">
|
|||
|
|
<div class="progress-fill" id="progressFill" style="width: 0%"></div>
|
|||
|
|
</div>
|
|||
|
|
<div class="progress-text" id="progressText">0 / 95 完成</div>
|
|||
|
|
</div>
|
|||
|
|
<div id="courseNav"></div>
|
|||
|
|
</aside>
|
|||
|
|
|
|||
|
|
<!-- Main Content: Task Area -->
|
|||
|
|
<main class="main-content">
|
|||
|
|
<!-- Welcome Panel -->
|
|||
|
|
<div class="welcome-panel" id="welcomePanel">
|
|||
|
|
<h1 class="welcome-title">🚀 开启 Linux 学习之旅</h1>
|
|||
|
|
<p class="welcome-desc">
|
|||
|
|
从入门到精通,系统学习 Linux 运维技能<br>
|
|||
|
|
<strong>12 个级别 · 95 道题目 · 循序渐进</strong>
|
|||
|
|
</p>
|
|||
|
|
<button class="start-btn" onclick="startLearning()">开始学习</button>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- Task Panel -->
|
|||
|
|
<div class="task-panel" id="taskPanel">
|
|||
|
|
<div class="task-header">
|
|||
|
|
<h2 class="task-title" id="taskTitle">任务标题</h2>
|
|||
|
|
<div class="task-meta">
|
|||
|
|
<span id="taskLevel">Level 1</span>
|
|||
|
|
<span>·</span>
|
|||
|
|
<span id="taskNumber">1 / 95</span>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="task-description" id="taskDescription">
|
|||
|
|
<!-- 任务描述 -->
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- Command Input -->
|
|||
|
|
<div class="command-area">
|
|||
|
|
<div class="command-input-wrapper">
|
|||
|
|
<span class="prompt">$</span>
|
|||
|
|
<input type="text" class="command-input" id="cmdInput"
|
|||
|
|
placeholder="输入 Linux 命令..."
|
|||
|
|
onkeypress="if(event.key==='Enter')executeCommand()">
|
|||
|
|
</div>
|
|||
|
|
<div class="command-output" id="cmdOutput">输出将显示在这里...</div>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<div class="action-buttons">
|
|||
|
|
<button class="btn btn-primary" onclick="executeCommand()">▶ 执行命令</button>
|
|||
|
|
<button class="btn btn-hint" onclick="showHint()">💡 提示</button>
|
|||
|
|
<button class="btn btn-secondary" onclick="showAnswer()">👀 查看答案</button>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<!-- Success Panel -->
|
|||
|
|
<div class="success-panel" id="successPanel">
|
|||
|
|
<div class="success-title">🎉 回答正确!</div>
|
|||
|
|
<p id="successMessage">恭喜你完成了这个任务!</p>
|
|||
|
|
<button class="deep-learning-btn" onclick="showDeepLearning()">
|
|||
|
|
📚 查看深入学习资料
|
|||
|
|
</button>
|
|||
|
|
<button class="btn btn-primary" onclick="nextTask()" style="margin-left: 1rem;">
|
|||
|
|
下一题 →
|
|||
|
|
</button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</main>
|
|||
|
|
|
|||
|
|
<!-- Right Sidebar: Learning Panel -->
|
|||
|
|
<aside class="learning-panel" id="learningPanel">
|
|||
|
|
<div class="panel-title">命令详解</div>
|
|||
|
|
<div id="commandDetail">
|
|||
|
|
<p style="color: var(--text-light); text-align: center; padding: 2rem 0;">
|
|||
|
|
选择一个任务开始学习<br>
|
|||
|
|
每个命令都有详细讲解
|
|||
|
|
</p>
|
|||
|
|
</div>
|
|||
|
|
</aside>
|
|||
|
|
</div>
|
|||
|
|
|
|||
|
|
<script>
|
|||
|
|
// ====================
|
|||
|
|
// 全局状态
|
|||
|
|
// ====================
|
|||
|
|
let COURSE_DATA = null;
|
|||
|
|
let currentMode = 'learn'; // 'learn' 或 'practice'
|
|||
|
|
let currentTask = null;
|
|||
|
|
let completedTasks = JSON.parse(localStorage.getItem('linux_completed') || '[]');
|
|||
|
|
let currentTheme = localStorage.getItem('linux_theme') || 'light';
|
|||
|
|
|
|||
|
|
// ====================
|
|||
|
|
// 命令知识库
|
|||
|
|
// ====================
|
|||
|
|
const COMMAND_KNOWLEDGE = {
|
|||
|
|
'pwd': {
|
|||
|
|
name: 'pwd',
|
|||
|
|
fullName: 'Print Working Directory',
|
|||
|
|
description: '显示当前工作目录的完整路径。这是你在文件系统中的"当前位置"。',
|
|||
|
|
commonParams: [
|
|||
|
|
{ param: '-L', desc: '显示逻辑路径(包括符号链接)' },
|
|||
|
|
{ param: '-P', desc: '显示物理路径(解析所有符号链接)' }
|
|||
|
|
],
|
|||
|
|
examples: [
|
|||
|
|
{ cmd: 'pwd', desc: '显示当前目录' },
|
|||
|
|
{ cmd: 'pwd -P', desc: '显示物理路径' }
|
|||
|
|
],
|
|||
|
|
tips: '当你不确定自己在哪个目录时,随时使用 pwd 确认位置。',
|
|||
|
|
related: ['cd', 'ls']
|
|||
|
|
},
|
|||
|
|
'ls': {
|
|||
|
|
name: 'ls',
|
|||
|
|
fullName: 'List Directory Contents',
|
|||
|
|
description: '列出目录中的文件和子目录。这是最常用的文件查看命令。',
|
|||
|
|
commonParams: [
|
|||
|
|
{ param: '-l', desc: '长格式显示,包含权限、所有者、大小等详细信息' },
|
|||
|
|
{ param: '-a', desc: '显示所有文件,包括以点开头的隐藏文件' },
|
|||
|
|
{ param: '-h', desc: '人类可读格式,文件大小显示为 K、M、G' },
|
|||
|
|
{ param: '-t', desc: '按修改时间排序' },
|
|||
|
|
{ param: '-r', desc: '反向排序' },
|
|||
|
|
{ param: '-S', desc: '按文件大小排序' }
|
|||
|
|
],
|
|||
|
|
examples: [
|
|||
|
|
{ cmd: 'ls', desc: '列出当前目录文件' },
|
|||
|
|
{ cmd: 'ls -la', desc: '详细列出所有文件' },
|
|||
|
|
{ cmd: 'ls -lh', desc: '详细列出,人类可读大小' },
|
|||
|
|
{ cmd: 'ls -lt', desc: '按时间排序列出' }
|
|||
|
|
],
|
|||
|
|
tips: 'ls -la 是查看目录内容最常用的组合命令。',
|
|||
|
|
related: ['pwd', 'cd', 'll']
|
|||
|
|
},
|
|||
|
|
'cd': {
|
|||
|
|
name: 'cd',
|
|||
|
|
fullName: 'Change Directory',
|
|||
|
|
description: '切换当前工作目录。这是文件系统导航的基础命令。',
|
|||
|
|
commonParams: [
|
|||
|
|
{ param: '..', desc: '切换到上级目录' },
|
|||
|
|
{ param: '~', desc: '切换到用户主目录' },
|
|||
|
|
{ param: '-', desc: '切换到刚才所在的目录' }
|
|||
|
|
],
|
|||
|
|
examples: [
|
|||
|
|
{ cmd: 'cd /home', desc: '切换到 /home 目录' },
|
|||
|
|
{ cmd: 'cd ..', desc: '切换到上级目录' },
|
|||
|
|
{ cmd: 'cd ~', desc: '切换到主目录' },
|
|||
|
|
{ cmd: 'cd -', desc: '返回刚才的目录' }
|
|||
|
|
],
|
|||
|
|
tips: 'cd - 可以快速在两个目录之间切换。',
|
|||
|
|
related: ['pwd', 'ls']
|
|||
|
|
},
|
|||
|
|
'cat': {
|
|||
|
|
name: 'cat',
|
|||
|
|
fullName: 'Concatenate',
|
|||
|
|
description: '连接文件并输出内容。常用于查看小文件的内容。',
|
|||
|
|
commonParams: [
|
|||
|
|
{ param: '-n', desc: '显示行号' },
|
|||
|
|
{ param: '-b', desc: '显示行号,但空行不编号' },
|
|||
|
|
{ param: '-s', desc: '压缩多个空行为一个' }
|
|||
|
|
],
|
|||
|
|
examples: [
|
|||
|
|
{ cmd: 'cat file.txt', desc: '显示文件内容' },
|
|||
|
|
{ cmd: 'cat -n file.txt', desc: '显示文件内容并带行号' },
|
|||
|
|
{ cmd: 'cat file1 file2', desc: '连接多个文件' }
|
|||
|
|
],
|
|||
|
|
tips: '对于大文件,建议使用 less 或 more 而不是 cat。',
|
|||
|
|
related: ['less', 'more', 'head', 'tail']
|
|||
|
|
}
|
|||
|
|
};
|
|||
|
|
|
|||
|
|
// ====================
|
|||
|
|
// 初始化
|
|||
|
|
// ====================
|
|||
|
|
document.addEventListener('DOMContentLoaded', async () => {
|
|||
|
|
applyTheme(currentTheme);
|
|||
|
|
await loadCourseData();
|
|||
|
|
renderCourseNav();
|
|||
|
|
updateProgress();
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
async function loadCourseData() {
|
|||
|
|
try {
|
|||
|
|
const res = await fetch('/api/tasks');
|
|||
|
|
if (res.ok) {
|
|||
|
|
COURSE_DATA = await res.json();
|
|||
|
|
console.log('✅ 课程数据加载成功');
|
|||
|
|
}
|
|||
|
|
} catch (e) {
|
|||
|
|
console.error('❌ 加载课程数据失败:', e);
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ====================
|
|||
|
|
// 模式切换
|
|||
|
|
// ====================
|
|||
|
|
function setMode(mode) {
|
|||
|
|
currentMode = mode;
|
|||
|
|
document.getElementById('learnMode').classList.toggle('active', mode === 'learn');
|
|||
|
|
document.getElementById('practiceMode').classList.toggle('active', mode === 'practice');
|
|||
|
|
|
|||
|
|
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');
|
|||
|
|
let html = '';
|
|||
|
|
|
|||
|
|
COURSE_DATA.levels.forEach((level, levelIdx) => {
|
|||
|
|
html += `
|
|||
|
|
<div class="level-section">
|
|||
|
|
<div class="level-header" onclick="toggleLevel(${levelIdx})">
|
|||
|
|
${level.title}
|
|||
|
|
</div>
|
|||
|
|
<ul class="task-list" id="level-${levelIdx}">
|
|||
|
|
`;
|
|||
|
|
|
|||
|
|
level.challenges.forEach((task, taskIdx) => {
|
|||
|
|
const isCompleted = completedTasks.includes(task.id);
|
|||
|
|
const isActive = currentTask && currentTask.id === task.id;
|
|||
|
|
|
|||
|
|
html += `
|
|||
|
|
<li class="task-item ${isActive ? 'active' : ''} ${isCompleted ? 'completed' : ''}"
|
|||
|
|
onclick="selectTask('${level.id}', '${task.id}')">
|
|||
|
|
<span class="task-status">${isCompleted ? '✅' : '○'}</span>
|
|||
|
|
<span>${taskIdx + 1}. ${task.title}</span>
|
|||
|
|
</li>
|
|||
|
|
`;
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
html += '</ul></div>';
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
nav.innerHTML = html;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function toggleLevel(levelIdx) {
|
|||
|
|
const list = document.getElementById(`level-${levelIdx}`);
|
|||
|
|
list.style.display = list.style.display === 'none' ? 'block' : 'none';
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function selectTask(levelId, taskId) {
|
|||
|
|
if (!COURSE_DATA) return;
|
|||
|
|
|
|||
|
|
const level = COURSE_DATA.levels.find(l => l.id === levelId);
|
|||
|
|
const task = level.challenges.find(t => t.id === taskId);
|
|||
|
|
|
|||
|
|
currentTask = { ...task, level: level.title, levelNum: COURSE_DATA.levels.indexOf(level) + 1 };
|
|||
|
|
renderTask(currentTask);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ====================
|
|||
|
|
// 任务渲染
|
|||
|
|
// ====================
|
|||
|
|
function renderTask(task) {
|
|||
|
|
document.getElementById('welcomePanel').style.display = 'none';
|
|||
|
|
document.getElementById('taskPanel').style.display = 'block';
|
|||
|
|
document.getElementById('successPanel').classList.remove('show');
|
|||
|
|
|
|||
|
|
// 基本信息
|
|||
|
|
document.getElementById('taskTitle').textContent = task.title;
|
|||
|
|
document.getElementById('taskLevel').textContent = task.level;
|
|||
|
|
document.getElementById('taskNumber').textContent = `${task.levelNum} / 95`;
|
|||
|
|
|
|||
|
|
// 任务描述
|
|||
|
|
let descHtml = `<h3>📝 任务描述</h3><p>${task.description}</p>`;
|
|||
|
|
|
|||
|
|
// 学习模式:显示更多学习资料
|
|||
|
|
if (currentMode === 'learn') {
|
|||
|
|
descHtml += `
|
|||
|
|
<div style="margin-top: 1rem; padding: 1rem; background: rgba(33,150,243,0.1); border-radius: 8px;">
|
|||
|
|
<strong>💡 学习提示:</strong> ${task.hint}
|
|||
|
|
</div>
|
|||
|
|
`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
document.getElementById('taskDescription').innerHTML = descHtml;
|
|||
|
|
|
|||
|
|
// 清空输出
|
|||
|
|
document.getElementById('cmdOutput').textContent = '输出将显示在这里...';
|
|||
|
|
document.getElementById('cmdInput').value = '';
|
|||
|
|
|
|||
|
|
// 更新学习面板
|
|||
|
|
updateLearningPanel(task);
|
|||
|
|
|
|||
|
|
// 更新导航高亮
|
|||
|
|
renderCourseNav();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function updateLearningPanel(task) {
|
|||
|
|
const panel = document.getElementById('commandDetail');
|
|||
|
|
|
|||
|
|
// 从任务描述中提取命令
|
|||
|
|
const cmdMatch = task.description.match(/<code>(\w+)<\/code>/);
|
|||
|
|
const cmdName = cmdMatch ? cmdMatch[1] : null;
|
|||
|
|
|
|||
|
|
if (cmdName && COMMAND_KNOWLEDGE[cmdName]) {
|
|||
|
|
const cmd = COMMAND_KNOWLEDGE[cmdName];
|
|||
|
|
let html = `
|
|||
|
|
<div class="command-detail active">
|
|||
|
|
<div class="cmd-name">${cmd.name}</div>
|
|||
|
|
<div style="color: var(--text-light); font-size: 0.9rem; margin-bottom: 1rem;">
|
|||
|
|
${cmd.fullName}
|
|||
|
|
</div>
|
|||
|
|
<div class="cmd-desc">${cmd.description}</div>
|
|||
|
|
|
|||
|
|
<div class="cmd-section">
|
|||
|
|
<div class="cmd-section-title">🔧 常用参数</div>
|
|||
|
|
<ul class="param-list">
|
|||
|
|
`;
|
|||
|
|
|
|||
|
|
cmd.commonParams.forEach(param => {
|
|||
|
|
html += `
|
|||
|
|
<li class="param-item">
|
|||
|
|
<span class="param-name">${param.param}</span>
|
|||
|
|
<span style="color: var(--text-light);"> - ${param.desc}</span>
|
|||
|
|
</li>
|
|||
|
|
`;
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
html += `</ul></div>
|
|||
|
|
<div class="cmd-section">
|
|||
|
|
<div class="cmd-section-title">📝 使用示例</div>
|
|||
|
|
`;
|
|||
|
|
|
|||
|
|
cmd.examples.forEach(ex => {
|
|||
|
|
html += `
|
|||
|
|
<div class="example-box">
|
|||
|
|
<code>${ex.cmd}</code>
|
|||
|
|
<div style="color: var(--text-light); margin-top: 0.3rem; font-size: 0.85rem;">
|
|||
|
|
${ex.desc}
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
`;
|
|||
|
|
});
|
|||
|
|
|
|||
|
|
html += `
|
|||
|
|
</div>
|
|||
|
|
<div class="cmd-section">
|
|||
|
|
<div class="cmd-section-title">💡 小贴士</div>
|
|||
|
|
<p style="font-size: 0.9rem; color: var(--text-light);">${cmd.tips}</p>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
`;
|
|||
|
|
|
|||
|
|
panel.innerHTML = html;
|
|||
|
|
} else {
|
|||
|
|
panel.innerHTML = `
|
|||
|
|
<div class="command-detail active">
|
|||
|
|
<p style="color: var(--text-light);">
|
|||
|
|
本任务涉及多个命令组合使用<br>
|
|||
|
|
完成任务后可查看详细解析
|
|||
|
|
</p>
|
|||
|
|
</div>
|
|||
|
|
`;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ====================
|
|||
|
|
// 命令执行
|
|||
|
|
// ====================
|
|||
|
|
async function executeCommand() {
|
|||
|
|
const cmd = document.getElementById('cmdInput').value.trim();
|
|||
|
|
if (!cmd) return;
|
|||
|
|
|
|||
|
|
const outputEl = document.getElementById('cmdOutput');
|
|||
|
|
outputEl.textContent = `执行: ${cmd}\n正在运行...`;
|
|||
|
|
|
|||
|
|
try {
|
|||
|
|
const res = await fetch('/api/run?cmd=' + encodeURIComponent(cmd));
|
|||
|
|
const data = await res.json();
|
|||
|
|
|
|||
|
|
outputEl.textContent = data.output || data.message || '(无输出)';
|
|||
|
|
|
|||
|
|
// 检查答案
|
|||
|
|
checkAnswer(cmd, data.output || '');
|
|||
|
|
} catch (e) {
|
|||
|
|
outputEl.textContent = `❌ 错误: ${e.message}`;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function checkAnswer(cmd, output) {
|
|||
|
|
if (!currentTask) return;
|
|||
|
|
|
|||
|
|
// 简化检查:命令是否包含在 solution 中
|
|||
|
|
const isCorrect = currentTask.solution && currentTask.solution.some(s =>
|
|||
|
|
cmd.toLowerCase().includes(s.toLowerCase())
|
|||
|
|
);
|
|||
|
|
|
|||
|
|
if (isCorrect) {
|
|||
|
|
showSuccess();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function showSuccess() {
|
|||
|
|
if (!completedTasks.includes(currentTask.id)) {
|
|||
|
|
completedTasks.push(currentTask.id);
|
|||
|
|
localStorage.setItem('linux_completed', JSON.stringify(completedTasks));
|
|||
|
|
updateProgress();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
document.getElementById('successPanel').classList.add('show');
|
|||
|
|
document.getElementById('successMessage').textContent =
|
|||
|
|
currentTask.success_msg || '恭喜你完成了这个任务!';
|
|||
|
|
|
|||
|
|
renderCourseNav();
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function showDeepLearning() {
|
|||
|
|
// 显示深入学习资料
|
|||
|
|
alert('📚 深入学习资料功能开发中...\n\n这里将显示:\n- 命令原理解析\n- 实际应用场景\n- 常见错误分析\n- 相关命令对比');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ====================
|
|||
|
|
// 辅助功能
|
|||
|
|
// ====================
|
|||
|
|
function showHint() {
|
|||
|
|
if (!currentTask) return;
|
|||
|
|
alert('💡 提示:\n\n' + currentTask.hint);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function showAnswer() {
|
|||
|
|
if (!currentTask) return;
|
|||
|
|
const answer = currentTask.solution ? currentTask.solution[0] : '暂无答案';
|
|||
|
|
|
|||
|
|
if (confirm('查看答案将标记此题为"已学习",确定要继续吗?')) {
|
|||
|
|
document.getElementById('cmdInput').value = answer;
|
|||
|
|
showSuccess();
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function nextTask() {
|
|||
|
|
if (!COURSE_DATA || !currentTask) return;
|
|||
|
|
|
|||
|
|
// 找到当前任务的下一个
|
|||
|
|
let found = false;
|
|||
|
|
for (const level of COURSE_DATA.levels) {
|
|||
|
|
for (const task of level.challenges) {
|
|||
|
|
if (found) {
|
|||
|
|
selectTask(level.id, task.id);
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
if (task.id === currentTask.id) found = true;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
alert('🎉 恭喜!你已完成所有课程!');
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function startLearning() {
|
|||
|
|
if (!COURSE_DATA) {
|
|||
|
|
alert('课程数据加载中,请稍候...');
|
|||
|
|
return;
|
|||
|
|
}
|
|||
|
|
selectTask(COURSE_DATA.levels[0].id, COURSE_DATA.levels[0].challenges[0].id);
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
function updateProgress() {
|
|||
|
|
const total = 95;
|
|||
|
|
const completed = completedTasks.length;
|
|||
|
|
const percent = Math.round((completed / total) * 100);
|
|||
|
|
|
|||
|
|
document.getElementById('progressFill').style.width = percent + '%';
|
|||
|
|
document.getElementById('progressText').textContent = `${completed} / ${total} 完成 (${percent}%)`;
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// ====================
|
|||
|
|
// 全局函数暴露
|
|||
|
|
// ====================
|
|||
|
|
window.setMode = setMode;
|
|||
|
|
window.toggleTheme = toggleTheme;
|
|||
|
|
window.toggleLevel = toggleLevel;
|
|||
|
|
window.selectTask = selectTask;
|
|||
|
|
window.executeCommand = executeCommand;
|
|||
|
|
window.showHint = showHint;
|
|||
|
|
window.showAnswer = showAnswer;
|
|||
|
|
window.showDeepLearning = showDeepLearning;
|
|||
|
|
window.nextTask = nextTask;
|
|||
|
|
window.startLearning = startLearning;
|
|||
|
|
</script>
|
|||
|
|
</body>
|
|||
|
|
</html>
|