fix: make linux stage navigation dynamic
This commit is contained in:
@@ -103,12 +103,12 @@
|
||||
const LANG={zh:{top:'Linux 运维学习实验室',title:'Linux 学习实验室',desc:'登录后在沙箱里学命令、做练习、看总览。',session:'当前用户',themeDark:'深色模式',themeLight:'浅色模式',logout:'退出登录',overview:'总览',nav:'课程快速入口',navText:'按阶段或模块快速跳转。',search:'搜索',searchBtn:'搜索',searchPlaceholder:'试试 pwd、grep 或 module_1',modules:'模块',all:'全部阶段',startLabel:'先选一节课开始',startTitle:'Linux 学习驾驶舱',startGoal:'从左侧选择课程,查看目标与练习。',summary:'模块摘要',runtime:'沙箱运行态',runtimeText:'下方可直接执行命令。',mark:'标记已掌握',review:'标记待复习',prev:'上一课',next:'下一课',why:'为什么重要',related:'相关命令',concepts:'核心概念',flow:'排障流程',practice:'练习',practiceText:'操作题直接在模拟终端中运行。',run:'运行',terminal:'Linux 沙箱终端',waiting:'等待输入命令...',empty:'暂无内容。',searchEmpty:'可以按命令名或课程 ID 搜索。',searchLoading:'正在搜索...',searchNo:'没有找到结果。',authExpired:'登录状态已失效,请重新登录。',logoutDone:'已退出登录,请重新登录。',stages:[{key:'all',title:'全部阶段'},{key:'stage-1',title:'阶段 1',range:[0,1]},{key:'stage-2',title:'阶段 2',range:[2,3]},{key:'stage-3',title:'阶段 3',range:[4,4]},{key:'stage-4',title:'阶段 4',range:[5,7]},{key:'stage-5',title:'阶段 5',range:[8,9]}]},en:{top:'Linux Ops Learning Lab',title:'Linux Learning Lab',desc:'Sign in to learn commands, practice in the sandbox, and navigate the course overview.',session:'Current user',themeDark:'Dark mode',themeLight:'Light mode',logout:'Log out',overview:'Overview',nav:'Course quick access',navText:'Jump quickly by stage or module.',search:'Search',searchBtn:'Search',searchPlaceholder:'Try pwd, grep, or module_1',modules:'Modules',all:'All stages',startLabel:'Pick a lesson to begin',startTitle:'Linux learning cockpit',startGoal:'Choose a lesson from the left to inspect goals and exercises.',summary:'Module summary',runtime:'Sandbox runtime',runtimeText:'Run commands directly below.',mark:'Mark mastered',review:'Mark for review',prev:'Previous',next:'Next',why:'Why this matters',related:'Related commands',concepts:'Core ideas',flow:'Troubleshooting flow',practice:'Practice',practiceText:'Operation tasks run inside the simulated terminal.',run:'Run',terminal:'Linux sandbox terminal',waiting:'Waiting for a command...',empty:'No content yet.',searchEmpty:'Search by command or lesson id.',searchLoading:'Searching...',searchNo:'No results found.',authExpired:'Session expired. Please sign in again.',logoutDone:'Logged out. Please sign in again.',stages:[{key:'all',title:'All stages'},{key:'stage-1',title:'Stage 1',range:[0,1]},{key:'stage-2',title:'Stage 2',range:[2,3]},{key:'stage-3',title:'Stage 3',range:[4,4]},{key:'stage-4',title:'Stage 4',range:[5,7]},{key:'stage-5',title:'Stage 5',range:[8,9]}]}};
|
||||
const state={overview:null,lesson:null,language:localStorage.getItem('linux_lab_language')||'zh',theme:localStorage.getItem('linux_lab_theme')||'light',progress:JSON.parse(localStorage.getItem('linux_lab_progress')||'{}'),user:'',stage:'all'}; const T=()=>LANG[state.language]; const tx=k=>D(T()[k]||'');
|
||||
const api=async(u,o={})=>{const r=await fetch(u,{credentials:'same-origin',...o}); if(r.status===401){localStorage.setItem('linux_lab_login_notice',tx('authExpired')); location.href='/login.html'; throw new Error(tx('authExpired'));} return r}; const save=()=>localStorage.setItem('linux_lab_progress',JSON.stringify(state.progress)); const mastered=id=>Boolean(state.progress[id]); const tokens=t=>Array.isArray(t?.command_tokens)&&t.command_tokens.length?t.command_tokens.filter(Boolean):String(t?.command||'').split('/').map(s=>s.trim()).filter(Boolean); const label=t=>tokens(t).join(' / ')||'command';
|
||||
const groups=()=>T().stages.map(g=>g.key==='all'?g:{...g,modules:(state.overview?.modules||[]).slice(g.range[0],g.range[1]+1)}); const visibleModules=()=>state.stage==='all'?(state.overview?.modules||[]):(groups().find(g=>g.key===state.stage)?.modules||[]);
|
||||
const buildStageGroups=()=>{const modules=state.overview?.modules||[];const stageCount=Math.max(1,Math.min(5,Math.ceil(modules.length/2)));const chunkSize=Math.max(1,Math.ceil(modules.length/stageCount));const allLabel=state.language==='zh'?'全部阶段':'All stages';const stagePrefix=state.language==='zh'?'阶段':'Stage';const groups=[{key:'all',title:allLabel,modules}];for(let i=0;i<stageCount;i+=1){const start=i*chunkSize;const end=Math.min(start+chunkSize,modules.length);if(start>=modules.length)break;groups.push({key:`stage-${i+1}`,title:`${stagePrefix} ${i+1}`,modules:modules.slice(start,end)});}return groups}; const groups=()=>buildStageGroups(); const visibleModules=()=>state.stage==='all'?(state.overview?.modules||[]):(groups().find(g=>g.key===state.stage)?.modules||[]);
|
||||
function setTheme(theme,persist=true){state.theme=theme==='dark'?'dark':'light';document.documentElement.setAttribute('data-theme',state.theme);if(persist)localStorage.setItem('linux_lab_theme',state.theme);$('themeBtn').textContent=state.theme==='light'?tx('themeDark'):tx('themeLight')}
|
||||
function setLanguage(lang,persist=true){state.language=lang==='en'?'en':'zh';if(persist)localStorage.setItem('linux_lab_language',state.language);renderText();if(state.overview)renderOverview();if(state.lesson)renderLesson()}
|
||||
function renderText(){document.title=tx('title');document.documentElement.lang=state.language==='zh'?'zh-CN':'en';$('topEyebrow').textContent=tx('top');$('courseTitle').textContent=tx('title');$('courseDesc').textContent=tx('desc');$('sessionUser').textContent=`${tx('session')}: ${state.user||'admin'}`;$('themeBtn').textContent=state.theme==='light'?tx('themeDark'):tx('themeLight');$('langBtn').textContent=state.language==='zh'?'EN':'\u4E2D\u6587';$('logoutBtn').textContent=tx('logout');$('overviewEyebrow').textContent=tx('overview');$('overviewTitle').textContent=tx('overview');$('searchEyebrow').textContent=tx('search');$('searchTitle').textContent=tx('search');$('searchBtn').textContent=tx('searchBtn');$('searchInput').placeholder=tx('searchPlaceholder');$('modulesEyebrow').textContent=tx('modules');$('modulesTitle').textContent=tx('modules');$('navEyebrow').textContent=tx('nav');$('navTitle').textContent=tx('nav');$('navText').textContent=tx('navText');$('lessonLabel').textContent=tx('startLabel');$('lessonTitle').textContent=tx('startTitle');$('lessonGoal').textContent=tx('startGoal');$('summaryTitle').textContent=tx('summary');$('runtimeTitle').textContent=tx('runtime');$('runtimeText').textContent=tx('runtimeText');$('masteryBtn').textContent=tx('mark');$('prevBtn').textContent=tx('prev');$('nextBtn').textContent=tx('next');$('whyTitle').textContent=tx('why');$('relatedTitle').textContent=tx('related');$('conceptsTitle').textContent=tx('concepts');$('flowTitle').textContent=tx('flow');$('practiceEyebrow').textContent=tx('practice');$('practiceTitle').textContent=tx('practice');$('practiceText').textContent=tx('practiceText')}
|
||||
function renderEmpty(){$('searchResults').innerHTML=`<div class=\"note\">${E(tx('searchEmpty'))}</div>`;$('moduleList').innerHTML=`<div class=\"note\">${E(tx('empty'))}</div>`;$('lessonCommands').innerHTML='';$('moduleSummary').textContent=tx('startGoal');$('masteryNote').textContent=tx('empty');$('relatedList').innerHTML='';$('conceptList').innerHTML=`<li>${E(tx('empty'))}</li>`;$('flowList').innerHTML=`<li>${E(tx('empty'))}</li>`;$('exampleList')&&($('exampleList').innerHTML='');$('exerciseList').innerHTML=`<div class=\"note\">${E(tx('empty'))}</div>`}
|
||||
function renderOverview(){const m=state.overview.meta||{};const labels=state.language==='zh'?['\u6A21\u5757','\u8BFE\u7A0B','\u7EC3\u4E60','\u547D\u4EE4','\u5DF2\u638C\u63E1','\u5B8C\u6210\u5EA6']:['Modules','Lessons','Exercises','Commands','Mastered','Completion'];const masteredCount=Object.values(state.progress).filter(Boolean).length;$('stats').innerHTML=[[labels[0],m.module_count||0],[labels[1],m.lesson_count||0],[labels[2],m.exercise_count||0],[labels[3],m.command_count||0],[labels[4],masteredCount],[labels[5],`${m.lesson_count?Math.round(masteredCount*100/m.lesson_count):0}%`]].map(([k,v])=>`<div class=\"stat\"><span>${E(k)}</span><strong>${v}</strong></div>`).join('');$('commandTags').innerHTML=(state.overview.commands||[]).map(c=>`<span class=\"chip\">${E(c)}</span>`).join('');$('metaLine').textContent=`v${m.version||'4.0'} | ${m.updated||'--'}`;$('runtimeMeta').textContent=`${state.language==='zh'?'\u6C99\u7BB1\u8DEF\u5F84':'Sandbox path'}: ${state.overview.runtime?.cwd||'/'} / ${state.overview.runtime?.user||'sandbox_user'}`;$('stageFilters').innerHTML=groups().map(g=>`<button class=\"filter ${state.stage===g.key?'active':''}\" type=\"button\" onclick=\"setStage('${g.key}')\">${E(D(g.title||g.key))}</button>`).join('');$('stageHint').textContent=state.stage==='all'?tx('navText'):D(T().stages.find(g=>g.key===state.stage)?.title||'');const modules=visibleModules();$('moduleJumps').innerHTML=modules.map(mod=>`<button class=\"ghost moduleJump ${state.lesson?.module_id===mod.id?'active':''}\" type=\"button\" onclick=\"openModule('${mod.id}')\">${E(mod.display_title||mod.title||mod.id)}</button>`).join('');$('moduleList').innerHTML=modules.map(mod=>`<button class=\"lesson ${state.lesson?.module_id===mod.id?'active':''}\" type=\"button\" onclick=\"openModule('${mod.id}')\"><strong>${E(mod.display_title||mod.title||mod.id)}</strong><span class=\"muted\">${E(mod.display_summary||mod.summary||'')}</span></button>`).join('')||`<div class=\"note\">${E(tx('empty'))}</div>`}
|
||||
function renderOverview(){const m=state.overview.meta||{};const labels=state.language==='zh'?['\u6A21\u5757','\u8BFE\u7A0B','\u7EC3\u4E60','\u547D\u4EE4','\u5DF2\u638C\u63E1','\u5B8C\u6210\u5EA6']:['Modules','Lessons','Exercises','Commands','Mastered','Completion'];const masteredCount=Object.values(state.progress).filter(Boolean).length;const stageGroups=groups();$('stats').innerHTML=[[labels[0],m.module_count||0],[labels[1],m.lesson_count||0],[labels[2],m.exercise_count||0],[labels[3],m.command_count||0],[labels[4],masteredCount],[labels[5],`${m.lesson_count?Math.round(masteredCount*100/m.lesson_count):0}%`]].map(([k,v])=>`<div class=\"stat\"><span>${E(k)}</span><strong>${v}</strong></div>`).join('');$('commandTags').innerHTML=(state.overview.commands||[]).map(c=>`<span class=\"chip\">${E(c)}</span>`).join('');$('metaLine').textContent=`v${m.version||'4.0'} | ${m.updated||'--'}`;$('runtimeMeta').textContent=`${state.language==='zh'?'\u6C99\u7BB1\u8DEF\u5F84':'Sandbox path'}: ${state.overview.runtime?.cwd||'/'} / ${state.overview.runtime?.user||'sandbox_user'}`;$('stageFilters').innerHTML=stageGroups.map(g=>`<button class=\"filter ${state.stage===g.key?'active':''}\" type=\"button\" onclick=\"setStage('${g.key}')\">${E(D(g.title||g.key))}</button>`).join('');$('stageHint').textContent=state.stage==='all'?tx('navText'):D(stageGroups.find(g=>g.key===state.stage)?.title||'');const modules=visibleModules();$('moduleJumps').innerHTML=modules.map(mod=>`<button class=\"ghost moduleJump ${state.lesson?.module_id===mod.id?'active':''}\" type=\"button\" onclick=\"openModule('${mod.id}')\">${E(mod.display_title||mod.title||mod.id)}</button>`).join('');$('moduleList').innerHTML=modules.map(mod=>`<button class=\"lesson ${state.lesson?.module_id===mod.id?'active':''}\" type=\"button\" onclick=\"openModule('${mod.id}')\"><strong>${E(mod.display_title||mod.title||mod.id)}</strong><span class=\"muted\">${E(mod.display_summary||mod.summary||'')}</span></button>`).join('')||`<div class=\"note\">${E(tx('empty'))}</div>`}
|
||||
function renderLesson(){if(!state.lesson)return;const l=state.lesson;const commandLabel=label(l);$('lessonLabel').textContent=l.display_module_title||l.module_id;$('lessonTitle').textContent=l.display_title||l.id;$('lessonGoal').textContent=state.language==='zh'?`\u56F4\u7ED5 ${commandLabel} \u7EC3\u4E60\u201C\u5148\u786E\u8BA4\u4E0A\u4E0B\u6587\uFF0C\u518D\u89C2\u5BDF\u8F93\u51FA\uFF0C\u518D\u51B3\u5B9A\u4E0B\u4E00\u6B65\u201D\u7684 Linux \u64CD\u4F5C\u601D\u8DEF\u3002`:(l.display_goal||`Practice a Linux workflow around ${commandLabel}: confirm context, inspect output, then decide the next step.`);$('moduleSummary').textContent=l.display_module_summary||'';$('masteryBtn').textContent=mastered(l.id)?tx('review'):tx('mark');$('masteryNote').textContent=mastered(l.id)?tx('review'):tx('mark');$('prevBtn').disabled=!l.previous_lesson;$('nextBtn').disabled=!l.next_lesson;$('lessonCommands').innerHTML=tokens(l).map(c=>`<span class=\"chip\">${E(c)}</span>`).join('');$('lessonWhy').textContent=state.language==='zh'?`\u5728\u771F\u5B9E\u8FD0\u7EF4\u91CC\uFF0C${commandLabel} \u5F80\u5F80\u4E0D\u662F\u7EC8\u70B9\uFF0C\u800C\u662F\u4E0B\u4E00\u6B65\u68C0\u67E5\u3001\u4FEE\u590D\u6216\u9A8C\u8BC1\u52A8\u4F5C\u7684\u8D77\u70B9\u3002`:(l.display_why||`In real operations work, ${commandLabel} often becomes the starting point for the next inspection, repair, or verification step.`);$('relatedList').innerHTML=(l.related_commands||[]).map(c=>`<span class=\"chip\">${E(c)}</span>`).join('')||`<span class=\"chip\">${E(tx('empty'))}</span>`;$('conceptList').innerHTML=(l.concepts||[]).map(i=>`<li>${E(i)}</li>`).join('')||`<li>${E(tx('empty'))}</li>`;$('flowList').innerHTML=(l.troubleshooting_flow||[]).map(i=>`<li>${E(i)}</li>`).join('')||`<li>${E(tx('empty'))}</li>`;renderExercises(l.exercises||[]);renderOverview()}
|
||||
function renderExercises(items){$('exerciseList').innerHTML=items.length?items.map(ex=>ex.type!=='operation'?`<article class=\"card\"><h3>${E(ex.question||ex.title||tx('practice'))}</h3><p class=\"muted\" style=\"margin-top:10px;white-space:pre-line;\">${E(ex.answer||tx('empty'))}</p></article>`:`<article class=\"card\"><div class=\"badge\">${E(tx('practice'))}</div><h3 style=\"margin-top:10px;\">${E(ex.title||tx('practice'))}</h3><p class=\"muted\" style=\"margin-top:10px;white-space:pre-line;\">${E(ex.hint||tx('empty'))}</p><div class=\"terminal\"><div class=\"terminalHead\">${E(tx('terminal'))}</div><div class=\"terminalRow\"><input id=\"input-${ex.id}\" type=\"text\" placeholder=\"${E((ex.solution||[])[0]||'pwd')}\" onkeydown=\"if(event.key==='Enter'){runExercise('${ex.id}')}\"><button type=\"button\" onclick=\"runExercise('${ex.id}')\">${E(tx('run'))}</button></div><pre class=\"output\" id=\"output-${ex.id}\">${E(tx('waiting'))}</pre></div><div class=\"note\" id=\"feedback-${ex.id}\" style=\"display:none;margin-top:10px;\"></div></article>`).join(''):`<div class=\"note\">${E(tx('empty'))}</div>`}
|
||||
async function openLesson(id){const r=await api('/api/lesson?id='+encodeURIComponent(id));const p=await r.json();state.lesson=p.lesson;renderLesson()}
|
||||
|
||||
Reference in New Issue
Block a user