128 lines
23 KiB
HTML
128 lines
23 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 Learning Lab</title>
|
|
<style>
|
|
:root{--bg:#f4f8fc;--panel:#fff;--line:#d8e2ee;--text:#102033;--muted:#5d7187;--brand:#0f6db6;--soft:#e9f3ff;--terminal:#0e1724}
|
|
[data-theme="dark"]{--bg:#08111d;--panel:#0c1826;--line:#1d3245;--text:#edf4fb;--muted:#9bb0c3;--brand:#67baff;--soft:rgba(103,186,255,.1);--terminal:#06101c}
|
|
*{box-sizing:border-box} body{margin:0;background:var(--bg);color:var(--text);font-family:"Segoe UI","Microsoft YaHei",sans-serif}
|
|
button,input{font:inherit} .shell{max-width:1400px;margin:0 auto;padding:16px} .card{background:var(--panel);border:1px solid var(--line);border-radius:18px;padding:16px}
|
|
.top,.toolbar,.chips,.filters{display:flex;flex-wrap:wrap;gap:10px}.top{justify-content:space-between;align-items:center;margin-bottom:16px}.layout{display:grid;grid-template-columns:320px 1fr;gap:16px}
|
|
.sidebar,.main{display:flex;flex-direction:column;gap:16px}.stats{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px}.stat{padding:12px;border:1px solid var(--line);border-radius:14px;background:var(--soft)} .stat span{display:block;font-size:12px;color:var(--muted)} .stat strong{font-size:22px}
|
|
.results,.modules{display:flex;flex-direction:column;gap:10px;max-height:34vh;overflow:auto}.btn,.ghost,.chip,.filter{border-radius:999px;padding:8px 12px;font-weight:700}.btn{border:0;background:linear-gradient(135deg,var(--brand),#37a1ff);color:#fff;cursor:pointer}.ghost,.filter{border:1px solid var(--line);background:transparent;color:var(--text);cursor:pointer}.chip{background:var(--soft);color:var(--brand);display:inline-flex}
|
|
.filter.active,.lesson.active,.moduleJump.active{background:var(--soft);border-color:rgba(15,109,182,.4);color:var(--brand)} .lesson,.searchBtn{width:100%;text-align:left;border:1px solid var(--line);background:transparent;border-radius:14px;padding:12px;cursor:pointer;color:var(--text)}
|
|
.search{display:flex;gap:8px}.search input,.terminal input{flex:1;padding:12px;border:1px solid var(--line);border-radius:12px;background:transparent;color:var(--text)} .grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:14px}
|
|
.terminal{margin-top:12px;border:1px solid var(--line);border-radius:14px;background:var(--terminal);overflow:hidden}.terminalHead,.output{color:#d8e8ff;font-family:Consolas,"Courier New",monospace}.terminalHead{padding:10px 12px;border-bottom:1px solid rgba(151,177,201,.16)}.terminalRow{display:flex;gap:8px;padding:12px}.terminalRow button{border:0;border-radius:10px;padding:8px 12px;background:linear-gradient(135deg,var(--brand),#37a1ff);color:#fff;cursor:pointer}.output{margin:0;min-height:96px;padding:12px;white-space:pre-wrap}.note{padding:10px 12px;border:1px solid var(--line);border-radius:12px;color:var(--muted)}
|
|
h1,h2,h3,p{margin:0}.eyebrow{font-size:12px;font-weight:800;letter-spacing:.12em;text-transform:uppercase;color:var(--brand)} .muted{color:var(--muted);line-height:1.8}
|
|
@media (max-width:1100px){.layout{grid-template-columns:1fr}.grid{grid-template-columns:1fr}}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<div class="shell">
|
|
<div class="card top">
|
|
<div>
|
|
<div class="eyebrow" id="topEyebrow"></div>
|
|
<h1 id="courseTitle"></h1>
|
|
<p class="muted" id="courseDesc"></p>
|
|
</div>
|
|
<div class="toolbar">
|
|
<span class="chip" id="sessionUser"></span>
|
|
<button class="ghost" id="themeBtn"></button>
|
|
<button class="ghost" id="langBtn"></button>
|
|
<button class="ghost" id="logoutBtn"></button>
|
|
</div>
|
|
</div>
|
|
<div class="layout">
|
|
<aside class="sidebar">
|
|
<section class="card">
|
|
<div class="eyebrow" id="overviewEyebrow"></div>
|
|
<h2 id="overviewTitle"></h2>
|
|
<div class="stats" id="stats"></div>
|
|
<div class="chips" id="commandTags" style="margin-top:12px;"></div>
|
|
<p class="muted" id="metaLine" style="margin-top:12px;"></p>
|
|
</section>
|
|
<section class="card">
|
|
<div class="eyebrow" id="searchEyebrow"></div>
|
|
<h2 id="searchTitle"></h2>
|
|
<div class="search" style="margin-top:12px;">
|
|
<input id="searchInput" type="text" />
|
|
<button class="btn" id="searchBtn"></button>
|
|
</div>
|
|
<div class="results" id="searchResults" style="margin-top:12px;"></div>
|
|
</section>
|
|
<section class="card">
|
|
<div class="eyebrow" id="modulesEyebrow"></div>
|
|
<h2 id="modulesTitle"></h2>
|
|
<div class="modules" id="moduleList" style="margin-top:12px;"></div>
|
|
</section>
|
|
</aside>
|
|
<main class="main">
|
|
<section class="card">
|
|
<div class="eyebrow" id="navEyebrow"></div>
|
|
<h2 id="navTitle"></h2>
|
|
<p class="muted" id="navText" style="margin-top:8px;"></p>
|
|
<div class="filters" id="stageFilters" style="margin-top:12px;"></div>
|
|
<p class="muted" id="stageHint" style="margin-top:12px;"></p>
|
|
<div class="filters" id="moduleJumps" style="margin-top:12px;"></div>
|
|
</section>
|
|
<section class="card">
|
|
<div class="eyebrow" id="lessonLabel"></div>
|
|
<h2 id="lessonTitle" style="margin-top:8px;"></h2>
|
|
<p class="muted" id="lessonGoal" style="margin-top:8px;"></p>
|
|
<div class="toolbar" style="margin-top:12px;">
|
|
<button class="ghost" id="masteryBtn"></button>
|
|
<button class="ghost" id="prevBtn"></button>
|
|
<button class="btn" id="nextBtn"></button>
|
|
</div>
|
|
<div class="chips" id="lessonCommands" style="margin-top:12px;"></div>
|
|
<div class="grid" style="margin-top:16px;">
|
|
<article class="card"><h3 id="summaryTitle"></h3><p class="muted" id="moduleSummary" style="margin-top:10px;"></p></article>
|
|
<article class="card"><h3 id="runtimeTitle"></h3><p class="muted" id="runtimeText" style="margin-top:10px;"></p><p class="muted" id="runtimeMeta" style="margin-top:10px;"></p></article>
|
|
</div>
|
|
<div class="note" id="masteryNote" style="margin-top:12px;"></div>
|
|
</section>
|
|
<section class="card grid">
|
|
<article class="card"><h3 id="whyTitle"></h3><p class="muted" id="lessonWhy" style="margin-top:10px;"></p></article>
|
|
<article class="card"><h3 id="relatedTitle"></h3><div class="chips" id="relatedList" style="margin-top:10px;"></div></article>
|
|
<article class="card"><h3 id="conceptsTitle"></h3><ul id="conceptList" class="muted"></ul></article>
|
|
<article class="card"><h3 id="flowTitle"></h3><ul id="flowList" class="muted"></ul></article>
|
|
</section>
|
|
<section class="card">
|
|
<div class="eyebrow" id="practiceEyebrow"></div>
|
|
<h2 id="practiceTitle"></h2>
|
|
<p class="muted" id="practiceText" style="margin-top:8px;"></p>
|
|
<div id="exerciseList" style="display:grid;gap:12px;margin-top:12px;"></div>
|
|
</section>
|
|
</main>
|
|
</div>
|
|
</div>
|
|
<script>
|
|
const D=v=>{const e=document.createElement('textarea');e.innerHTML=v;return e.value};const $=id=>document.getElementById(id);const E=v=>String(v).replaceAll('&','&').replaceAll('<','<').replaceAll('>','>').replaceAll('\"','"').replaceAll("'",''');
|
|
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||[]);
|
|
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 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()}
|
|
function openModule(id){const m=(state.overview?.modules||[]).find(x=>x.id===id);const l=m?.lessons?.[0];if(l)openLesson(l.id)}
|
|
function setStage(key){state.stage=key;renderOverview()}
|
|
function toggleMastery(){if(!state.lesson?.id)return;state.progress[state.lesson.id]=!state.progress[state.lesson.id];save();renderLesson()}
|
|
async function runSearch(){const q=$('searchInput').value.trim();if(!q){$('searchResults').innerHTML=`<div class=\"note\">${E(tx('searchEmpty'))}</div>`;return}$('searchResults').innerHTML=`<div class=\"note\">${E(tx('searchLoading'))}</div>`;const r=await api('/api/course/search?q='+encodeURIComponent(q));const p=await r.json();$('searchResults').innerHTML=(p.results||[]).length?(p.results||[]).map(it=>`<button class=\"searchBtn\" type=\"button\" onclick=\"openLesson('${it.lesson_id}')\"><strong>${E(it.title)}</strong><span class=\"muted\">${E(it.subtitle||'')}</span></button>`).join(''):`<div class=\"note\">${E(tx('searchNo'))}</div>`}
|
|
async function runExercise(id){const input=$('input-'+id),out=$('output-'+id),fb=$('feedback-'+id),cmd=input.value.trim();if(!cmd)return;out.textContent=`$ ${cmd}\\n\\n...`;const rr=await api('/api/run?cmd='+encodeURIComponent(cmd));const rd=await rr.json();out.textContent=`$ ${cmd}\\n\\n${rd.output||rd.message||''}`;const cr=await api('/api/check?exercise_id='+encodeURIComponent(id)+'&last_cmd='+encodeURIComponent(cmd)+'&output='+encodeURIComponent(rd.output||''));const cd=await cr.json();fb.style.display='block';fb.textContent=cd.message+(cd.next_suggestion?'\\n'+cd.next_suggestion:'')}
|
|
async function logoutLab(){try{await fetch('/api/logout',{method:'POST',credentials:'same-origin'})}catch(e){}localStorage.setItem('linux_lab_login_notice',tx('logoutDone'));location.href='/login.html'}
|
|
async function boot(){const s=await fetch('/api/session',{credentials:'same-origin'});const p=await s.json();if(!p.authenticated){localStorage.setItem('linux_lab_login_notice',tx('authExpired'));location.href='/login.html';return}state.user=p.user||'admin';renderText();renderEmpty();setTheme(state.theme,false);const r=await api('/api/overview');state.overview=await r.json();renderOverview();const first=state.overview?.modules?.[0]?.lessons?.[0];if(first)await openLesson(first.id)}
|
|
document.addEventListener('DOMContentLoaded',()=>{$('themeBtn').addEventListener('click',()=>setTheme(state.theme==='light'?'dark':'light'));$('langBtn').addEventListener('click',()=>setLanguage(state.language==='zh'?'en':'zh'));$('logoutBtn').addEventListener('click',logoutLab);$('searchBtn').addEventListener('click',runSearch);$('searchInput').addEventListener('keydown',e=>{if(e.key==='Enter')runSearch()});$('masteryBtn').addEventListener('click',toggleMastery);$('prevBtn').addEventListener('click',()=>{if(state.lesson?.previous_lesson)openLesson(state.lesson.previous_lesson.id)});$('nextBtn').addEventListener('click',()=>{if(state.lesson?.next_lesson)openLesson(state.lesson.next_lesson.id)});boot()});
|
|
window.openLesson=openLesson;window.openModule=openModule;window.setStage=setStage;window.runExercise=runExercise;
|
|
</script>
|
|
</body>
|
|
</html>
|
|
|