Files
memind/public/learn/parent.html
T
john f9e105acd0 feat(learn): add standalone /learn pages backed by Page Data
Serve lightweight child/parent learning assistant pages at /learn without
MindSpace injection, with a rebuild script and Portal static route.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-15 17:17:18 +08:00

117 lines
22 KiB
HTML
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
<title>家长后台</title>
<meta name="mindspace-page-data-page-id" content="f9c80392-9417-46fd-af89-a91a239d3bfc">
<script>window.__MINDSPACE_PAGE_DATA__={"pageId":"f9c80392-9417-46fd-af89-a91a239d3bfc","accessMode":"password"};</script>
<style>*{box-sizing:border-box;margin:0;padding:0;-webkit-tap-highlight-color:transparent}
body{font-family:-apple-system,BlinkMacSystemFont,"PingFang SC",sans-serif;background:#f2f4fa;color:#1c2333;min-height:100vh;padding-bottom:70px;padding-bottom:calc(70px + constant(safe-area-inset-bottom));padding-bottom:calc(70px + env(safe-area-inset-bottom))}
.wrap{max-width:560px;margin:0 auto;padding:16px 14px}
header.h{padding:16px 2px 4px}
h1{font-size:22px;font-weight:700}
.sub{font-size:13px;color:#6b7280;margin-top:3px}
.card{background:#fff;border-radius:16px;padding:16px;margin-top:12px;box-shadow:0 1px 3px rgba(28,35,51,.06)}
.big{font-size:26px;font-weight:700}
.bar{height:10px;background:#e8ebf2;border-radius:5px;overflow:hidden;margin-top:8px}
.bar i{display:block;height:100%;background:linear-gradient(90deg,#22c55e,#16a34a);border-radius:5px;transition:width .4s}
.cat{margin-top:14px}
.cat-title{font-size:13px;font-weight:700;padding:3px 10px;border-radius:999px;display:inline-flex;align-items:center;gap:5px}
.task{display:flex;align-items:center;gap:12px;padding:12px 2px;border-bottom:1px solid #f0f2f7;cursor:pointer}
.task:last-child{border-bottom:none}
.task .ck{width:26px;height:26px;border-radius:50%;border:2px solid #cbd5e1;flex:none;display:flex;align-items:center;justify-content:center;font-size:15px;color:#fff}
.task.done .ck{background:#16a34a;border-color:#16a34a}
.task .info{flex:1;min-width:0}
.task .t{font-size:15px;font-weight:600}
.task.done .t{color:#9ca3af;text-decoration:line-through}
.task .d{font-size:12px;color:#6b7280;margin-top:2px}
.task .pt{font-size:12px;color:#f59e0b;font-weight:700;flex:none}
.tabbar{position:fixed;left:0;right:0;bottom:0 !important;top:auto !important;background:#fff;border-top:1px solid #e5e8f0;display:flex;z-index:20;padding:6px 0 8px;padding:6px 0 calc(8px + constant(safe-area-inset-bottom));padding:6px 0 calc(8px + env(safe-area-inset-bottom))}
.overlay{top:0 !important;left:0 !important;right:0 !important;bottom:0 !important}
.tabbar a{flex:1;text-align:center;text-decoration:none;color:#9ca3af;font-size:11px;padding:4px 0}
.tabbar a.on{color:#3b5bfd;font-weight:700}
.tabbar .ic{font-size:20px;display:block}
button{font-family:inherit;border:none;cursor:pointer;border-radius:12px;font-size:14px;font-weight:600;padding:10px 16px;background:#3b5bfd;color:#fff}
button:active{transform:scale(.97)}
button.ghost{background:#e8edff;color:#3b5bfd}
button.danger{background:#ffe9ee;color:#e11d48}
button:disabled{opacity:.4;cursor:not-allowed}
input,select,textarea{font-family:inherit;width:100%;border:2px solid #e5e8f0;border-radius:10px;padding:10px 12px;font-size:15px;outline:none;background:#fff;color:#1c2333}
input:focus,select:focus,textarea:focus{border-color:#3b5bfd}
label{display:block;font-size:13px;font-weight:600;margin:12px 0 5px}
.overlay{position:fixed;inset:0;background:rgba(20,24,38,.5);z-index:40;display:flex;align-items:center;justify-content:center;padding:20px}
.modal{background:#fff;border-radius:18px;padding:24px;width:100%;max-width:360px;max-height:85vh;overflow:auto}
.statcheck{display:grid;grid-template-columns:repeat(3,1fr);gap:10px;margin-top:12px}
.sc{background:#fff;border-radius:14px;padding:13px 8px;text-align:center;box-shadow:0 1px 3px rgba(28,35,51,.06)}
.sc .n{font-size:20px;font-weight:700}
.sc .l{font-size:11px;color:#6b7280;margin-top:2px}
.cal{display:grid;grid-template-columns:repeat(7,1fr);gap:6px;margin-top:10px}
.cal .w{text-align:center;font-size:11px;color:#9ca3af;padding:4px 0}
.cal .cell{text-align:center;padding:7px 0;border-radius:10px;font-size:14px;position:relative}
.cal .cell.has{font-weight:700}
.cal .cell.g{background:#e7f8ee;color:#16a34a}.cal .cell.y{background:#fff4dd;color:#d97706}.cal .cell.r{background:#ffe9ee;color:#e11d48}.cal .cell.n{color:#c3c9d6}
.cal .today{outline:2px solid #3b5bfd}
.legend{display:flex;gap:12px;font-size:11px;color:#6b7280;margin-top:10px;flex-wrap:wrap}
.dot{display:inline-block;width:9px;height:9px;border-radius:50%;margin-right:4px;vertical-align:-1px}
.feed{position:fixed;left:50%;top:38%;transform:translate(-50%,-50%);background:#fff;border-radius:20px;padding:28px 30px;text-align:center;z-index:50;box-shadow:0 10px 40px rgba(28,35,51,.25);animation:pop .3s}
@keyframes pop{from{transform:translate(-50%,-50%) scale(.8);opacity:0}to{transform:translate(-50%,-50%) scale(1);opacity:1}}
.feed .em{font-size:44px}.feed .t1{font-size:18px;font-weight:700;margin-top:8px}.feed .t2{font-size:13px;color:#6b7280;margin-top:6px;line-height:1.6}
.feed .pt{color:#f59e0b;font-weight:700;margin-top:8px;font-size:15px}
table{width:100%;border-collapse:collapse;font-size:13px}
th,td{padding:8px 6px;border-bottom:1px solid #f0f2f7;text-align:left}
th{font-size:11px;color:#6b7280}
.row{display:flex;gap:10px;align-items:center}
.row>*{flex:1}
.pw{height:8px;background:#e8ebf2;border-radius:4px;overflow:hidden;min-width:60px}
.pw i{display:block;height:100%;background:#3b5bfd}
.chip{display:inline-flex;align-items:center;gap:6px;padding:6px 14px;border-radius:999px;border:2px solid #e5e8f0;font-size:13px;cursor:pointer;margin:0 8px 8px 0;background:#fff}
.chip.on{border-color:#3b5bfd;background:#e8edff;color:#3b5bfd;font-weight:700}
.hbtn{display:inline-flex;align-items:center;gap:4px;font-size:12px;font-weight:600;color:#3b5bfd;background:#e8edff;padding:6px 13px;border-radius:999px;text-decoration:none;cursor:pointer;flex:none;white-space:nowrap}
.hbtn:active{transform:scale(.95)}
.err{background:#ffe9ee;color:#e11d48;padding:10px;border-radius:10px;font-size:13px;margin-top:10px;display:none}
.tabs{display:flex;gap:8px;margin-top:14px}
.tabs button{flex:1;background:#e8ebf2;color:#6b7280}
.tabs button.on{background:#3b5bfd;color:#fff}
.empty{text-align:center;color:#9ca3af;font-size:13px;padding:18px 0;line-height:1.7}body{padding-bottom:30px}.tabs{position:sticky;top:0;background:#f2f4fa;padding-top:2px;z-index:10}.pl{max-width:720px}</style>
<script src="/assets/page-data-client.js?v=20260915j"></script>
<script src="/learn/learning-assistant-data.js?v=20260915j"></script>
<link rel="icon" type="image/png" href="/brand/tkmind-icon.png">
</head>
<body>
<div class="wrap pl" id="app"><div class="card"><div class="empty">加载中…</div></div></div>
<script>
var state={pw:localStorage.getItem('learn_pw')||'',tab:'dash',stats:null,templates:null,categories:null,children:null,editId:null};
function esc(s){return String(s==null?'':s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;')}
function pad(n){return n<10?'0'+n:''+n}
var WD=['一','二','三','四','五','六','日'];
function weekdaysCN(w){return LearnStore.parseWeekdays(w).map(function(x){return WD[+x-1]}).join('、')}
var TYPE={check:'完成型',count:'数量型',duration:'时长型',score:'成绩型'};
function renderLogin(){return '<header class="h"><h1>家长后台 🔐</h1><div class="sub">任务配置 · 数据统计 · 习惯分析</div></header><div class="card"><label>管理密码</label><input type="password" id="pw" placeholder="请输入密码" onkeydown="if(event.key===\'Enter\')login()"><div class="err" id="err"></div><button style="width:100%;margin-top:16px" onclick="login()">进入后台</button><div class="sub" style="text-align:center;margin-top:14px"><a href="/learn/" style="color:#3b5bfd">← 返回小朋友页面</a></div></div>'}
async function login(){state.pw=document.getElementById('pw').value;try{await LearnStore.verifyPassword(state.pw);localStorage.setItem('learn_pw',state.pw);state.tab='dash';await loadAll();render()}catch(e){var el=document.getElementById('err');el.style.display='block';el.textContent=e.message}}
function renderDash(){var s=state.stats;if(!s)return '<header class="h"><h1>数据看板 📊</h1></header><div class="card"><div class="empty">加载中…</div></div>';var tp=s.today.total?Math.round(s.today.done/s.today.total*100):0;var wp=s.week.total?Math.round(s.week.done/s.week.total*100):0;var st=s.children.length?s.children[0].streak:0;var html='<header class="h"><h1>数据看板 📊</h1><div class="sub">'+s.children.map(function(c){return esc(c.emoji)+' '+esc(c.name)+' 连续 '+c.streak+' 天🔥'}).join(' ')+'</div></header><div class="statcheck"><div class="sc"><div class="n" style="color:'+(tp===100?'#16a34a':'#3b5bfd')+'">'+tp+'%</div><div class="l">今日完成率 '+(s.today.done+'/'+s.today.total)+'</div></div><div class="sc"><div class="n">'+wp+'%</div><div class="l">本周完成率</div></div><div class="sc"><div class="n" style="color:#f59e0b">'+st+'</div><div class="l">连续全勤(天)</div></div></div><div class="card"><div class="t" style="font-weight:700;font-size:15px;margin-bottom:8px">近 7 天趋势</div>';s.trend.forEach(function(d){var p=d.total?Math.round(d.done/d.total*100):0;html+='<div style="margin:8px 0"><div class="row" style="align-items:center"><span style="flex:0 0 58px;font-size:12px;color:#6b7280">'+d.d.slice(5)+'</span><div class="pw" style="flex:1"><i style="width:'+p+'%;background:'+(p===100?'#16a34a':'#3b5bfd')+'"></i></div><span style="flex:0 0 40px;text-align:right;font-size:12px;color:#6b7280">'+p+'%</span></div></div>'});html+='</div>';if(s.missed.length){html+='<div class="card"><div class="t" style="font-weight:700;font-size:15px;margin-bottom:8px">⚠️ 近 30 天容易遗漏</div><table>';s.missed.forEach(function(m){html+='<tr><td>'+esc(m.title)+'</td><td style="color:#e11d48;font-weight:700;text-align:right">漏 '+m.count+' 次</td></tr>'});html+='</table></div>'}if(s.categoryRate.length){html+='<div class="card"><div class="t" style="font-weight:700;font-size:15px;margin-bottom:8px">📚 分类完成率(近 30 天)</div>';s.categoryRate.forEach(function(c){var p=c.total?Math.round(c.done/c.total*100):0;html+='<div style="margin:8px 0"><div class="row" style="align-items:center"><span style="flex:0 0 64px;font-size:13px;font-weight:600">'+esc(c.name)+'</span><div class="pw" style="flex:1"><i style="width:'+p+'%;background:'+(p>=90?'#16a34a':p>=70?'#f59e0b':'#e11d48')+'"></i></div><span style="flex:0 0 40px;text-align:right;font-size:12px;color:#6b7280">'+p+'%</span></div></div>'});html+='</div>'}if(s.avgTime.length){html+='<div class="card"><div class="t" style="font-weight:700;font-size:15px;margin-bottom:8px">⏰ 平均完成时间(近 30 天)</div><table>';s.avgTime.forEach(function(a){html+='<tr><td>'+esc(a.name)+'</td><td style="text-align:right;font-weight:600">'+a.time+'</td></tr>'});html+='</table><div class="sub" style="margin-top:8px">提示:越晚完成的科目,遗漏概率往往越高</div></div>'}return html}
function childLabel(id){var c=state.children.find(function(x){return x.id===id});return c?esc(c.emoji)+' '+esc(c.name):''}
function renderTemplates(){var html='<header class="h"><h1>任务模板 📋</h1><div class="sub">配置周期规则,系统每天自动生成当日任务</div></header>';if(!state.templates||!state.templates.length)html+='<div class="card"><div class="empty">还没有任务模板<br>点下方按钮创建第一个</div></div>';state.templates.forEach(function(t){html+='<div class="card"><div class="row" style="align-items:flex-start"><div style="flex:1"><div style="font-size:16px;font-weight:700">'+esc(t.title)+'</div><div class="sub" style="margin-top:4px;line-height:1.7">👦 '+childLabel(t.child_id)+' '+(t.category_name?'【'+esc(t.category_name)+'】 ':'')+TYPE[t.task_type]+(t.target_value?' · '+esc(t.target_value)+(t.unit?esc(t.unit):''):'')+'<br>每周:'+weekdaysCN(t.weekdays)+' 从 '+String(t.start_date).slice(0,10)+' 起'+(t.end_date?' 至 '+String(t.end_date).slice(0,10):'(长期)')+'<br>+'+t.points+' 能量'+(t.description?'<br>要求:'+esc(t.description):'')+'</div></div><div style="display:flex;flex-direction:column;gap:8px;flex:0 0 66px"><button class="ghost" style="padding:7px 10px;font-size:12px" onclick="editTpl('+t.id+')">编辑</button><button class="danger" style="padding:7px 10px;font-size:12px" onclick="delTpl('+t.id+',\''+esc(t.title).replace(/'/g,'')+'\')">停用</button></div></div></div>'});html+='<button style="width:100%;padding:14px;margin-top:14px" onclick="newTpl()"> 新建任务模板</button>';return html}
function tplForm(t){var kids;if(t){var cur=state.children.find(function(c){return c.id===t.child_id});kids='<div style="margin-top:2px"><span class="chip on" style="cursor:default">'+esc(cur?cur.emoji+' '+cur.name:'')+'</span><input type="hidden" id="f_child" value="'+t.child_id+'"></div>'}else{kids='<div style="margin-top:2px">'+state.children.map(function(c){return '<span class="chip child on" data-id="'+c.id+'" onclick="this.classList.toggle(\'on\')">'+esc(c.emoji)+' '+esc(c.name)+'</span>'}).join('')+'</div>'}var cats=state.categories.map(function(c){return '<option value="'+c.id+'"'+(t&&t.category_id===c.id?' selected':'')+'>'+esc(c.name)+'</option>'}).join('');var wds=LearnStore.parseWeekdays(t?t.weekdays:'1,2,3,4,5');var wchk=WD.map(function(w,i){return '<span class="chip wd'+(wds.indexOf(String(i+1))>=0?' on':'')+'" onclick="this.classList.toggle(\'on\')">'+w+'</span>'}).join('');return '<div class="overlay" onclick="if(event.target===this)this.remove()"><div class="modal"><h3 style="font-size:17px;margin-bottom:4px">'+(t?'编辑任务模板':'新建任务模板')+'</h3><label>'+(t?'给谁安排':'给谁安排(可多选)')+'</label>'+kids+'<label>分类</label><select id="f_cat"><option value="">(无分类)</option>'+cats+'</select><label>任务名称 *</label><input id="f_title" value="'+(t?esc(t.title):'')+'" placeholder="如:英语单词复习" maxlength="60"><label>任务要求(孩子可见)</label><textarea id="f_desc" rows="2" maxlength="300" style="font-size:14px">'+(t?esc(t.description||''):'')+'</textarea><label>任务类型</label><select id="f_type">'+['check','count','duration','score'].map(function(k){return '<option value="'+k+'"'+(t&&t.task_type===k?' selected':'')+'>'+TYPE[k]+'</option>'}).join('')+'</select><div class="row"><div><label>目标值</label><input id="f_target" value="'+(t?esc(t.target_value||''):'')+'" placeholder="如 20" inputmode="numeric"></div><div><label>单位</label><input id="f_unit" value="'+(t?esc(t.unit||''):'')+'" placeholder="如 题/分钟/个"></div></div><label>重复的星期(点选)</label><div style="margin-top:2px">'+wchk+'</div><div class="row"><div><label>开始日期</label><input id="f_start" type="date" value="'+(t?String(t.start_date).slice(0,10):new Date().toISOString().slice(0,10))+'"></div><div><label>结束日期(可空)</label><input id="f_end" type="date" value="'+(t&&t.end_date?String(t.end_date).slice(0,10):'')+'"></div></div><label>能量值(完成奖励)</label><input id="f_points" type="number" value="'+(t?t.points:10)+'" min="0" max="100"><div class="err" id="f_err"></div><div class="row" style="margin-top:18px"><button class="ghost" onclick="this.closest(\'.overlay\').remove()">取消</button><button onclick="saveTpl('+(t?t.id:0)+')">保存</button></div></div></div>'}
function newTpl(){document.getElementById('app').insertAdjacentHTML('beforeend',tplForm(null))}
function editTpl(id){var t=state.templates.find(function(x){return x.id===id});if(t)document.getElementById('app').insertAdjacentHTML('beforeend',tplForm(t))}
async function delTpl(id,name){if(!confirm('停用「'+name+'」?已生成的历史任务保留,今后不再生成。'))return;try{await LearnStore.parentDelete('templates',id);await loadAll();render()}catch(e){alert(e.message||'停用失败')}}
async function saveTpl(id){var wds=[];document.querySelectorAll('.overlay .chip.wd.on').forEach(function(c){wds.push(String(WD.indexOf(c.textContent)+1))});var body={category_id:+document.getElementById('f_cat').value||null,title:document.getElementById('f_title').value,description:document.getElementById('f_desc').value,task_type:document.getElementById('f_type').value,target_value:document.getElementById('f_target').value,unit:document.getElementById('f_unit').value,weekdays:wds.join(','),start_date:document.getElementById('f_start').value,end_date:document.getElementById('f_end').value,points:+document.getElementById('f_points').value};try{if(!body.title)throw new Error('任务名称必填');if(!wds.length)throw new Error('请至少选择一个星期');if(id){body.child_id=+document.getElementById('f_child').value;await LearnStore.parentUpdate('templates',id,body)}else{var childIds=[];document.querySelectorAll('.overlay .chip.child.on').forEach(function(c){childIds.push(+c.getAttribute('data-id'))});if(!childIds.length)throw new Error('请至少选择一个小朋友');await LearnStore.parentInsertMany('templates',childIds.map(function(cid){return Object.assign({},body,{child_id:cid})}))}document.querySelector('.overlay').remove();await loadAll();render()}catch(e){var el=document.getElementById('f_err');el.style.display='block';el.textContent=e.message}}
function renderCats(){var html='<header class="h"><h1>分类管理 🏷️</h1><div class="sub">学习 + 习惯,不限语数外</div></header><div class="card"><table>';state.categories.forEach(function(c){html+='<tr><td><span class="dot" style="background:'+esc(c.color)+'"></span>'+esc(c.name)+'</td><td style="text-align:right"><button class="danger" style="padding:5px 10px;font-size:12px" onclick="delCat('+c.id+',\''+esc(c.name)+'\')">删除</button></td></tr>'});html+='</table></div><div class="card"><label>新增分类</label><div class="row"><input id="c_name" placeholder="如:练琴" maxlength="10"><input id="c_color" type="color" style="flex:0 0 56px;padding:4px" value="#3b5bfd"><button style="flex:0 0 70px" onclick="addCat()">添加</button></div></div>';return html}
async function addCat(){var n=document.getElementById('c_name').value.trim();if(!n)return;await LearnStore.parentInsert('categories',{name:n,color:document.getElementById('c_color').value});await loadAll();render()}
async function delCat(id,name){if(!confirm('删除分类「'+name+'」?使用它的模板将变为无分类。'))return;try{await LearnStore.parentDelete('categories',id);await loadAll();render()}catch(e){alert(e.message||'删除失败')}}
function renderKids(){var html='<header class="h"><h1>孩子管理 👦</h1><div class="sub">儿童端选择孩子后进入</div></header>';state.children.forEach(function(c){html+='<div class="card"><div class="row"><div style="flex:1;font-size:16px;font-weight:700">'+esc(c.emoji)+' '+esc(c.name)+'</div><button class="danger" style="flex:0 0 66px;padding:7px 0;font-size:12px" onclick="delKid('+c.id+',\''+esc(c.name)+'\')">删除</button></div></div>'});html+='<div class="card"><label>添加孩子</label><div class="row"><input id="k_emoji" placeholder="😀" style="flex:0 0 64px" maxlength="2"><input id="k_name" placeholder="昵称,如:小宇" maxlength="10"><button style="flex:0 0 70px" onclick="addKid()">添加</button></div></div>';return html}
async function addKid(){var n=document.getElementById('k_name').value.trim();if(!n)return;await LearnStore.parentInsert('children',{name:n,emoji:document.getElementById('k_emoji').value||'🙂'});await loadAll();render()}
async function delKid(id,name){if(!confirm('删除「'+name+'」及其所有任务记录?此操作不可恢复!'))return;try{await LearnStore.parentDelete('children',id);await loadAll();render()}catch(e){alert(e.message||'删除失败')}}
function renderSet(){return '<header class="h"><h1>设置 ⚙️</h1></header><div class="card"><label>修改管理密码</label><input type="password" id="p_old" placeholder="当前密码"><input type="password" id="p_new" placeholder="新密码(至少3位)" style="margin-top:8px"><div class="err" id="p_err"></div><button style="width:100%;margin-top:14px" onclick="chpw()">保存新密码</button></div><div class="card"><div class="sub" style="line-height:1.8">· 儿童端地址:<a href="/learn/" style="color:#3b5bfd">打开</a><br>· 任务每天首次访问时自动生成<br>· 历史任务保存快照,改模板不影响过去记录<br>· 数据存储:Page Data(口令默认 88888888</div></div><button class="danger" style="width:100%;padding:12px" onclick="logout()">退出登录</button>'}
async function chpw(){try{await LearnStore.parentPassword(document.getElementById('p_old').value,document.getElementById('p_new').value);alert('密码已更新');state.pw=document.getElementById('p_new').value;localStorage.setItem('learn_pw',state.pw)}catch(e){var el=document.getElementById('p_err');el.style.display='block';el.textContent=e.message}}
function logout(){localStorage.removeItem('learn_pw');if(window.LearnStore&&LearnStore.clearAuth)LearnStore.clearAuth();state.pw='';render()}
async function loadAll(){if(state.tab==='dash')state.stats=await LearnStore.parentStats();if(state.tab==='tpl'){state.templates=(await LearnStore.parentList('templates')).data;state.categories=(await LearnStore.parentList('categories')).data;state.children=(await LearnStore.parentList('children')).data}if(state.tab==='cat'){state.categories=(await LearnStore.parentList('categories')).data}if(state.tab==='kid'){state.children=(await LearnStore.parentList('children')).data}}
function render(){var app=document.getElementById('app');if(!state.pw){app.innerHTML=renderLogin();return}var tabs=[['dash','📊','看板'],['tpl','📋','模板'],['cat','🏷️','分类'],['kid','👦','孩子'],['set','⚙️','设置']];var body=state.tab==='dash'?renderDash():state.tab==='tpl'?renderTemplates():state.tab==='cat'?renderCats():state.tab==='kid'?renderKids():renderSet();app.innerHTML=body+'<div class="tabs">'+tabs.map(function(t){return '<button class="'+(state.tab===t[0]?'on':'')+'" onclick="go(\''+t[0]+'\')">'+t[1]+' '+t[2]+'</button>'}).join('')+'</div>'}
async function go(tab){state.tab=tab;try{await loadAll();render()}catch(e){state.pw='';localStorage.removeItem('learn_pw');render()}}
(async function(){if(state.pw){try{await LearnStore.init({password:state.pw});await loadAll()}catch(e){state.pw='';localStorage.removeItem('learn_pw')}}try{render()}catch(e){document.getElementById('app').innerHTML='<div class="card"><div class="empty">'+esc(e.message)+'</div></div>'}})();
</script>
</body>
</html>