@@ -590,17 +659,17 @@
- Stage map
- 阶段路线清单
- 按阶段划分的学习路线,帮你把命令练习串联成一条完整的运维路径。
+ Stage map
+ 阶段路线清单
+ 按阶段划分的学习路线,帮你把命令练习串联成一条完整的运维路径。
- Command categories
- 命令家族可视化
- 了解各类命令在当前课程中的分布,以及建议的学习顺序。
+ Command categories
+ 命令家族可视化
+ 了解各类命令在当前课程中的分布,以及建议的学习顺序。
@@ -1343,6 +1412,7 @@
document.getElementById('practiceText').textContent = text.practiceText;
document.getElementById('prevBtn').textContent = text.prevBtn;
document.getElementById('nextBtn').textContent = text.nextBtn;
+ renderAuthControls();
if (!state.overview) {
document.getElementById('courseTitle').textContent = text.courseTitle;
@@ -1389,6 +1459,7 @@
renderStaticText();
document.getElementById('themeBtn').addEventListener('click', () => setTheme(state.theme === 'light' ? 'dark' : 'light'));
document.getElementById('languageBtn').addEventListener('click', () => setLanguage(state.language === 'zh' ? 'en' : 'zh'));
+ document.getElementById('logoutBtn').addEventListener('click', logoutLab);
document.getElementById('searchInput').addEventListener('keydown', (event) => {
if (event.key === 'Enter') runSearch();
});
@@ -1399,8 +1470,12 @@
if (state.lesson?.next_lesson) openLesson(state.lesson.next_lesson.id);
});
document.getElementById('masteryBtn').addEventListener('click', toggleMastery);
- await loadOverview();
- renderAuthStatus();
+ if (state.auth.loggedIn) {
+ hideAuthGate();
+ await loadOverview();
+ } else {
+ showAuthGate();
+ }
});
function saveProgress() {
@@ -1434,24 +1509,55 @@
localStorage.setItem('linux_lab_auth', JSON.stringify(state.auth));
}
- function renderAuthStatus() {
- const badge = document.getElementById('authStatusBadge');
- const modeLabel = document.getElementById('authModeLabel');
- const hint = document.getElementById('authHint');
- const form = document.getElementById('loginForm');
+ function renderAuthControls() {
+ const zh = state.language === 'zh';
const logoutBtn = document.getElementById('logoutBtn');
- badge.textContent = state.auth.loggedIn ? '已登录' : '公开访问';
- modeLabel.textContent = state.auth.loggedIn ? `当前登录:${state.auth.user}` : '未登录';
- hint.textContent = state.auth.loggedIn
- ? '您已解锁全部实验和进度保存功能。'
- : '登录后可解锁任务追踪、保存进度和补全排障建议提示。';
- form.style.display = state.auth.loggedIn ? 'none' : 'flex';
logoutBtn.style.display = state.auth.loggedIn ? 'inline-flex' : 'none';
- badge.classList.toggle('auth-status', !!state.auth.loggedIn);
+ logoutBtn.textContent = zh ? '退出登录' : 'Log out';
+
+ document.getElementById('navEyebrow').textContent = zh ? '总览菜单' : 'Overview menu';
+ document.getElementById('navTitle').textContent = zh ? '课程总览' : 'Course overview';
+ document.getElementById('navText').textContent = zh ? '按模块、阶段或练习快速跳转,不再只能上一课下一课。' : 'Jump by module, stage, or exercise instead of relying only on previous and next.';
+ document.getElementById('navStageCountLabel').textContent = zh ? '已设置阶段' : 'Mapped stages';
+ document.getElementById('navExerciseCountLabel').textContent = zh ? '练习总数' : 'Exercises';
+ document.getElementById('navLessonEmpty').textContent = zh ? '登录后即可选择模块导航。' : 'Sign in to unlock module navigation.';
+ document.getElementById('navStageEyebrow').textContent = zh ? '阶段地图' : 'Stage map';
+ document.getElementById('navCategoryEyebrow').textContent = zh ? '命令分类' : 'Command categories';
+ document.getElementById('stageMapEyebrow').textContent = zh ? '阶段地图' : 'Stage map';
+ document.getElementById('stageMapTitle').textContent = zh ? '阶段路线清单' : 'Stage route checklist';
+ document.getElementById('stageMapText').textContent = zh ? '按阶段划分的学习路线,帮你把命令练习串联成一条完整的运维路径。' : 'Study by stages so commands connect into a complete operations path.';
+ document.getElementById('categoryEyebrow').textContent = zh ? '命令分类' : 'Command categories';
+ document.getElementById('categoryTitle').textContent = zh ? '命令家族可视化' : 'Command family map';
+ document.getElementById('categoryText').textContent = zh ? '了解各类命令在当前课程中的分布,以及建议的学习顺序。' : 'See how command families are distributed and which order makes sense.';
+ document.getElementById('gateTitle').textContent = zh ? '请先登录' : 'Sign in first';
+ document.getElementById('gateDescription').textContent = zh ? '登录后解锁课程总览、阶段地图、命令分类和练习仪表盘。' : 'Sign in to unlock the overview, stage map, command categories, and practice dashboard.';
+ document.getElementById('gateUser').setAttribute('placeholder', zh ? '用户名' : 'Username');
+ document.getElementById('gatePassword').setAttribute('placeholder', zh ? '密码' : 'Password');
+ document.getElementById('gateSubmit').textContent = zh ? '登录并进入总览' : 'Login and enter overview';
+ if (!state.auth.loggedIn) {
+ document.getElementById('gateMessage').textContent = zh ? '将自动保存进度与阶段状态,方便继续学习。' : 'Progress and stage state will be saved automatically so you can resume later.';
+ }
+ }
+
+ async function apiFetch(path, options = {}) {
+ const headers = new Headers(options.headers || {});
+ if (state.auth.token) {
+ headers.set('Authorization', `Bearer ${state.auth.token}`);
+ headers.set('X-Token', state.auth.token);
+ }
+ return fetch(path, Object.assign({}, options, { headers }));
+ }
+
+ function showAuthGate() {
+ document.getElementById('authGate').classList.remove('hidden');
+ }
+
+ function hideAuthGate() {
+ document.getElementById('authGate').classList.add('hidden');
}
async function loadOverview() {
- const response = await fetch('/api/overview');
+ const response = await apiFetch('/api/overview');
state.overview = await response.json();
renderOverview();
const firstLesson = state.overview.modules?.[0]?.lessons?.[0];
@@ -1482,6 +1588,7 @@
renderModules();
renderStageGrid();
renderModuleSummaryGrid();
+ renderVisualModules();
}
function renderRuntime(runtime) {
@@ -1570,7 +1677,7 @@
}
async function openLesson(lessonId, focusExerciseId = '') {
- const response = await fetch('/api/lesson?id=' + encodeURIComponent(lessonId));
+ const response = await apiFetch('/api/lesson?id=' + encodeURIComponent(lessonId));
const payload = await response.json();
state.lesson = payload.lesson;
renderModules();
@@ -1750,11 +1857,11 @@
feedback.className = 'feedback';
feedback.textContent = '';
try {
- const runResponse = await fetch('/api/run?cmd=' + encodeURIComponent(cmd));
+ const runResponse = await apiFetch('/api/run?cmd=' + encodeURIComponent(cmd));
const runData = await runResponse.json();
output.textContent = `$ ${cmd}\n\n${runData.output || runData.message || text.noOutput}`;
renderRuntime({ cwd: runData.cwd || '/', user: state.overview?.runtime?.user || 'sandbox_user' });
- const checkResponse = await fetch('/api/check?exercise_id=' + encodeURIComponent(exerciseId) + '&last_cmd=' + encodeURIComponent(cmd) + '&output=' + encodeURIComponent(runData.output || ''));
+ const checkResponse = await apiFetch('/api/check?exercise_id=' + encodeURIComponent(exerciseId) + '&last_cmd=' + encodeURIComponent(cmd) + '&output=' + encodeURIComponent(runData.output || ''));
const checkData = await checkResponse.json();
feedback.className = 'feedback show ' + (checkData.success ? 'success' : 'warn');
feedback.textContent = checkData.message + (checkData.next_suggestion ? '\n' + checkData.next_suggestion : '');
@@ -1767,7 +1874,7 @@
async function resetSandbox() {
const text = uiText();
- await fetch('/api/reset', { method: 'POST' });
+ await apiFetch('/api/reset', { method: 'POST' });
renderRuntime({ cwd: '/', user: 'sandbox_user' });
alert(text.sandboxResetComplete);
}
@@ -1782,7 +1889,7 @@
}
container.innerHTML = `
${escapeHtml(text.searchLoading)}
`;
try {
- const response = await fetch('/api/course/search?q=' + encodeURIComponent(query));
+ const response = await apiFetch('/api/course/search?q=' + encodeURIComponent(query));
const payload = await response.json();
const results = payload.results || [];
if (!results.length) {
@@ -1846,6 +1953,19 @@
const percent = totalLessons ? Math.round((masteredLessons / totalLessons) * 100) : 0;
return `
${escapeHtml(localizedModuleTitle(module))}: ${percent}%`;
}).join("");
+ const navStageMap = document.getElementById("navStageMap");
+ const navStageCount = document.getElementById("navStageCount");
+ if (navStageMap) {
+ navStageMap.innerHTML = modules.map((module, index) => `
+
+ ${index + 1}. ${escapeHtml(localizedModuleTitle(module))}
+ ${(module.lesson_count || 0)} lessons
+
+ `).join("");
+ }
+ if (navStageCount) {
+ navStageCount.textContent = modules.length;
+ }
}
function renderCategoryGrid() {
@@ -1873,13 +1993,38 @@
`).join("");
}
+ function renderNavLessons() {
+ const container = document.getElementById("navLessonResults");
+ const modules = state.overview?.modules || [];
+ const exerciseCount = modules.reduce((sum, module) => sum + (module.exercise_count || 0), 0);
+ const lessonCountEl = document.getElementById("navLessonCount");
+ const exerciseCountEl = document.getElementById("navExerciseCount");
+ if (lessonCountEl) {
+ lessonCountEl.textContent = modules.length;
+ }
+ if (exerciseCountEl) {
+ exerciseCountEl.textContent = exerciseCount;
+ }
+ if (!modules.length) {
+ container.innerHTML = `
${escapeHtml(uiText().noModulesFound)}
`;
+ return;
+ }
+ container.innerHTML = modules.map((module) => `
+
+ `).join("");
+ }
+
async function loginToLab(event) {
event.preventDefault();
- const username = document.getElementById("authUser").value.trim();
- const password = document.getElementById("authPassword").value.trim();
- const messageNode = document.getElementById("authMessage");
+ const zh = state.language === 'zh';
+ const username = document.getElementById("gateUser").value.trim();
+ const password = document.getElementById("gatePassword").value.trim();
+ const messageNode = document.getElementById("gateMessage");
if (!username || !password) {
- messageNode.textContent = "请输入用户名和密码";
+ messageNode.textContent = zh ? "请输入用户名和密码" : "Please enter a username and password.";
return;
}
try {
@@ -1891,41 +2036,52 @@
const payload = await response.json();
if (payload.success) {
state.auth = { loggedIn: true, user: username, token: payload.token };
- messageNode.textContent = payload.message || "登录成功";
- document.getElementById("authStatusBadge").textContent = "已登录";
- document.getElementById("authModeLabel").textContent = `当前登录:${username}`;
- document.getElementById("logoutBtn").style.display = "inline-flex";
- document.getElementById("loginForm").style.display = "none";
+ persistAuth();
+ hideAuthGate();
+ messageNode.textContent = payload.message || (zh ? "登录成功" : "Login succeeded.");
+ renderAuthControls();
+ await loadOverview();
return;
}
- messageNode.textContent = payload.error || "登录失败";
+ messageNode.textContent = payload.error || (zh ? "登录失败" : "Login failed.");
} catch (error) {
- messageNode.textContent = "网络异常,请稍后再试";
+ messageNode.textContent = zh ? "网络异常,请稍后再试" : "Network error. Please try again later.";
}
}
function logoutLab() {
+ const zh = state.language === 'zh';
state.auth = { loggedIn: false, user: "", token: "" };
- document.getElementById("authMessage").textContent = "已退出";
- document.getElementById("authStatusBadge").textContent = "仅公开";
- document.getElementById("authModeLabel").textContent = "未登录";
- document.getElementById("logoutBtn").style.display = "none";
- document.getElementById("loginForm").style.display = "flex";
+ persistAuth();
+ showAuthGate();
+ renderAuthControls();
+ document.getElementById("gateMessage").textContent = zh ? "已退出,重新登录即可继续。" : "Logged out. Sign in again to continue.";
}
function renderVisualModules() {
renderStageTimeline();
renderCategoryGrid();
+ renderNavLessons();
}
- document.getElementById("loginForm").addEventListener("submit", loginToLab);
- document.getElementById("logoutBtn").addEventListener("click", logoutLab);
+ document.getElementById("gateLoginForm").addEventListener("submit", loginToLab);
window.openLesson = openLesson;
window.runExercise = runExercise;
window.runSearch = runSearch;
window.resetSandbox = resetSandbox;
- renderVisualModules();
+
+
+
请先登录
+
登录后解锁课程总览、阶段地图、命令分类和练习仪表盘。
+
+
将自动保存进度与阶段状态,方便继续学习。
+
+