#!/usr/bin/env node /** * 生成唐用户学习小助手 HTML(儿童端 + 家长端) */ import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { createLearnAssistantService, learnAssistantInternals } from '../learn-assistant-service.mjs'; const root = path.join(path.dirname(fileURLToPath(import.meta.url)), '..'); const TANG = learnAssistantInternals.TANG_OWNER_ID; const API_BASE = '/api/learn/tang'; const outDir = path.join(root, 'MindSpace', TANG, 'public'); const sharedCss = `*{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} [data-mindspace-public-share],[data-mindspace-wechat-dialog],.publication-share-fab,.publication-share-sheet{display:none!important;visibility:hidden!important}`; function childScript() { return ` var API_BASE='${API_BASE}'; var state={tab:'today',child:null,children:[],data:null,cal:null,grow:null}; function esc(s){return String(s==null?'':s).replace(/&/g,'&').replace(//g,'>')} async function api(p,opt){var r=await fetch(API_BASE+p,opt);var j=await r.json().catch(function(){return{}});if(!r.ok)throw new Error(j.error||'请求失败');return j} function pickChild(){if(state.children.length<=1)return state.children[0]?state.children[0].id:1;var saved=localStorage.getItem('learn_child');var c=state.children.find(function(x){return String(x.id)===saved});if(!c){var s='

我是谁呀?

';state.children.forEach(function(x){s+='
'+esc(x.emoji)+' '+esc(x.name)+'
'});s+='
';return s}return c.id} function chooseChild(id){localStorage.setItem('learn_child',String(id));location.reload()} function showSwitcher(){var s='
';document.body.insertAdjacentHTML('beforeend',s)} function hdrActions(){var cur=state.children.find(function(x){return x.id==state.child});return '
⇄ '+(cur?'切换学员':'学员')+'家长端
'} function greet(){var h=new Date().getHours();return h<11?'早上好':h<14?'中午好':h<18?'下午好':'晚上好'} function pad(n){return n<10?'0'+n:''+n} function wdCN(d){return ['日','一','二','三','四','五','六'][d]} function dowOf(ds){return new Date(Date.UTC(+ds.slice(0,4),+ds.slice(5,7)-1,+ds.slice(8,10))).getUTCDay()} function renderToday(){var d=state.data;var p=d.total?Math.round(d.done/d.total*100):0;var s='

'+greet()+','+esc(d.child.name)+' '+esc(d.child.emoji)+'

'+d.date.replace(/-/g,'/')+' 周'+wdCN(dowOf(d.date))+'
'+hdrActions()+'
'+d.done+' / '+d.total+'
今天已完成
⭐ '+d.energy+'
我的能量
'+(d.total&&d.done===d.total?'
⭐ 今天全部完成,太棒啦!
':'')+'
';var byCat={},order=[];d.tasks.forEach(function(t){var k=t.category_name||'其他';if(!byCat[k]){byCat[k]={color:t.category_color||'#6b7280',list:[]};order.push(k)}byCat[k].list.push(t)});if(!d.tasks.length)s+='
今天没有任务
好好休息,或找爸爸妈妈安排~
';order.forEach(function(k){s+='
'+esc(k)+'
';byCat[k].list.forEach(function(t){var needVal=(t.task_type!=='check');var sub=[];if(t.description)sub.push(esc(t.description));if(needVal)sub.push('目标:'+esc(t.target_value||'')+(t.unit?esc(t.unit):''));if(t.start_date)sub.push('📅 '+t.start_date.replace(/-/g,'/')+' 开始');if(t.status==='done'){sub.unshift('✓ '+(t.completed_time||'')+' 完成'+(needVal&&t.value_submitted?','+esc(t.value_submitted)+(t.unit?esc(t.unit):''):''));}s+='
'+(t.status==='done'?'✓':'')+'
'+esc(t.title)+'
'+(sub.length?'
'+sub.join('
')+'
':'')+'
+'+t.points+'
'});s+='
'});if(d.streak>0)s+='
🔥 连续打卡 '+d.streak+' 天,继续保持!
';return s} function renderCal(){var m=state.cal.month;var first=m+'-01';var dim=new Date(Date.UTC(+m.slice(0,4),+m.slice(5,7),0)).getUTCDate();var lead=dowOf(first);var map={};state.cal.days.forEach(function(x){map[x.date]=x});var s='

我的日历 📅

'+m.replace('-',' 年 ')+' 月
'+hdrActions()+'
';['日','一','二','三','四','五','六'].forEach(function(w){s+='
'+w+'
'});for(var i=0;i
';var today=new Date(Date.now()+8*3600e3).toISOString().slice(0,10);for(var d=1;d<=dim;d++){var ds=m+'-'+pad(d);var st=map[ds];var cls='cell ';var mark='';if(st){cls+=st.status==='green'?'g':st.status==='yellow'?'y':st.status==='red'?'r':'n';if(st.total){cls+=' has';if(st.status==='red')mark='';else if(st.status==='yellow')mark='';}}if(ds===today)cls+=' today';s+='
'+d+mark+'
'}s+='
全部完成部分完成有遗漏(红点)无任务
点任意日期查看当天详情
';return s} async function showDay(ds){var j=await api('/day?child='+state.child+'&date='+ds);var el=document.getElementById('dayDetail');var isToday=ds===new Date(Date.now()+8*3600e3).toISOString().slice(0,10);var stTxt='';var stColor='';var done=j.tasks.filter(function(t){return t.status==='done'}).length;if(j.tasks.length){if(done===j.tasks.length){stTxt='全部完成 ✓';stColor='#16a34a'}else if(done>0){stTxt='完成 '+done+'/'+j.tasks.length+(ds'+(stTxt?''+stTxt+'':'')+'';if(!j.tasks.length)s+='
那天没有任务
';j.tasks.forEach(function(t){var overdue=t.status==='pending'&&ds
'+(t.status==='done'?'✓':(overdue?'✕':''))+'
'+esc(t.title)+'
'+esc(t.category_name||'')+(t.status==='done'?' · '+(t.completed_time||'')+' 完成':(overdue?' · 未完成':' · 待完成')+(t.value_submitted?' · '+esc(t.value_submitted)+(t.unit||''):''))+'
'});if(j.tasks.length)s+='
完成率:'+Math.round(done/j.tasks.length*100)+'%
';s+='';el.innerHTML=s} function renderGrow(){var g=state.grow;var s='

我的成长 🌱

'+hdrActions()+'
⭐ '+g.energy+'
我的能量
🔥 '+g.streak+'
连续打卡
'+g.totalDone+'
累计完成
💪 历史最长连续:'+g.best+' 天 | 全勤天数:'+g.fullDays+' 天
近 30 天分类表现
';if(!g.byCategory.length)s+='
还没有记录,快去完成任务吧!
';g.byCategory.forEach(function(c){var p=c.total?Math.round(c.done/c.total*100):0;s+='
'+esc(c.name)+'
'+p+'%
'});s+='
';return s} function renderHist(){var h=state.hist;var today=new Date(Date.now()+8*3600e3).toISOString().slice(0,10);var s='

我的记录 📖

每天的任务和完成情况,都记在这里'+(h.cycleStart?'
本期任务从 '+h.cycleStart.replace(/-/g,'/')+' 开始':'')+'
'+hdrActions()+'
';if(!h.data.length)s+='
还没有记录
完成今天的任务,就会出现在这里哦
';var daysAsc=h.data.slice().reverse();daysAsc.forEach(function(day,idx){var label=day.date===today?'今天':(day.date===addDaysLocal(today,-1)?'昨天':(parseInt(day.date.slice(5),10))+'月'+parseInt(day.date.slice(8),10)+'日');var w='周'+wdCN(dowOf(day.date));var stTxt=day.status==='green'?'全部完成':(day.done>0?'完成 '+day.done+'/'+day.total:(day.date
'+label+' '+w+' '+dot+'
'+stTxt+''+(open?'▾':'▸')+'
';day.tasks.forEach(function(t){var sub=[];if(t.status==='done')sub.push('✓ '+(t.completed_time||'')+' 完成'+(t.task_type!=='check'&&t.value_submitted?','+esc(t.value_submitted)+(t.unit?esc(t.unit):''):''));else if(t.status==='pending'&&day.date
'+(t.status==='done'?'✓':(t.status==='pending'&&day.date
'+esc(t.title)+'
'+esc(t.category_name||'')+' · '+sub.join(' · ')+'
'});s+='
'});return s} function toggleDay(id,el){var b=document.getElementById(id);var arr=el.querySelector('.arr')||el.parentElement.querySelector('.arr');if(b.style.display==='none'){b.style.display='block';if(arr)arr.textContent='▾'}else{b.style.display='none';if(arr)arr.textContent='▸'}} function addDaysLocal(ds,n){var d=new Date(ds+'T00:00:00Z');d.setUTCDate(d.getUTCDate()+n);return d.toISOString().slice(0,10)} function renderTabbar(){var t=[['today','☀️','今日'],['hist','📖','记录'],['cal','📅','日历'],['grow','🌱','成长']];return t.map(function(x){return ''+x[1]+''+x[2]+''}).join('')} async function go(tab){state.tab=tab;await load();render()} async function load(){if(!state.children.length){state.children=(await api('/children')).data}if(state.child==null){var c=pickChild();if(typeof c!=='number')return;state.child=c}if(state.child==null)return;if(state.tab==='today')state.data=await api('/today?child='+state.child);if(state.tab==='hist')state.hist=await api('/history?child='+state.child);if(state.tab==='cal'&&!state.cal)state.cal=await api('/calendar?child='+state.child);if(state.tab==='grow')state.grow=await api('/growth?child='+state.child)} function render(){var app=document.getElementById('app');if(!state.children.length){app.innerHTML='
还没有小朋友的信息
请爸爸妈妈先去 家长后台 添加
';document.getElementById('tabbar').innerHTML='';return}if(state.child==null){app.innerHTML=pickChild();document.getElementById('tabbar').innerHTML='';return}var s=state.tab==='today'?renderToday():state.tab==='hist'?renderHist():state.tab==='cal'?renderCal():renderGrow();app.innerHTML=s;document.getElementById('tabbar').innerHTML=renderTabbar()} async function calShift(n){var m=state.cal.month.split('-');var y=+m[0],mo=+m[1]+n;if(mo<1){mo=12;y--}if(mo>12){mo=1;y++}state.cal=await api('/calendar?child='+state.child+'&month='+y+'-'+pad(mo));render()} function doTask(id){var t=null;state.data.tasks.forEach(function(x){if(x.id===id)t=x});if(!t)(state.data.pastPending||[]).forEach(function(x){if(x.id===id)t=x});if(!t)return;if(t.status==='done')return;if(t.task_type==='check'){submit(id)}else{var unit=t.unit||'';var label=t.task_type==='count'?'完成了多少'+(unit?esc(unit):''):(t.task_type==='duration'?'坚持了多少'+(unit||'分钟'):'得了多少分');var def=t.value_submitted||(t.task_type==='duration'?(t.target_value||''):(t.target_value||''));document.getElementById('app').insertAdjacentHTML('beforeend','
');var inp=document.getElementById('vInput');if(inp)inp.focus()}} function submitVal(id){var v=document.getElementById('vInput').value;var r=document.getElementById('vRemark').value;submit(id,v,r)} async function submit(id,value,remark){try{var j=await api('/checkin',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({id:id,value:value,remark:remark})});var s='
🎉
完成啦!
今天已经完成 '+j.done+' / '+j.total+' 项
'+(j.allDone?'⭐ 太棒了,今天全部完成!':'再完成 '+(j.total-j.done)+' 项,今天就是全绿!')+'
+'+j.points+' 能量
点一下继续
';document.body.insertAdjacentHTML('beforeend',s);await load();render()}catch(e){alert(e.message)}} (async function(){try{await load();render()}catch(e){document.getElementById('app').innerHTML='
'+esc(e.message)+'
'}})();`; } function parentScript() { return ` var API_BASE='${API_BASE}'; 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,'&').replace(//g,'>')} async function api(p,opt){opt=opt||{};opt.headers=Object.assign({'X-Parent-Pw':state.pw,'X-Admin-Pw':state.pw,'Content-Type':'application/json'},opt.headers||{});var r=await fetch(API_BASE+p,opt);var j=await r.json().catch(function(){return{}});if(!r.ok)throw new Error(j.error||'请求失败');return j} function pad(n){return n<10?'0'+n:''+n} var WD=['一','二','三','四','五','六','日']; function weekdaysCN(w){return w.split(',').map(function(x){return WD[+x-1]}).join('、')} var TYPE={check:'完成型',count:'数量型',duration:'时长型',score:'成绩型'}; function renderLogin(){return '

家长后台 🔐

任务配置 · 数据统计 · 习惯分析
'} async function login(){state.pw=document.getElementById('pw').value;try{await api('/parent/stats');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;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='

数据看板 📊

'+s.children.map(function(c){return esc(c.emoji)+' '+esc(c.name)+' 连续 '+c.streak+' 天🔥'}).join(' ')+'
'+tp+'%
今日完成率 '+(s.today.done+'/'+s.today.total)+'
'+wp+'%
本周完成率
'+st+'
连续全勤(天)
近 7 天趋势
';s.trend.forEach(function(d){var p=d.total?Math.round(d.done/d.total*100):0;html+='
'+d.d.slice(5)+'
'+p+'%
'});html+='
';if(s.missed.length){html+='
⚠️ 近 30 天容易遗漏
';s.missed.forEach(function(m){html+=''});html+='
'+esc(m.title)+'漏 '+m.count+' 次
'}if(s.categoryRate.length){html+='
📚 分类完成率(近 30 天)
';s.categoryRate.forEach(function(c){var p=c.total?Math.round(c.done/c.total*100):0;html+='
'+esc(c.name)+'
'+p+'%
'});html+='
'}if(s.avgTime.length){html+='
⏰ 平均完成时间(近 30 天)
';s.avgTime.forEach(function(a){html+=''});html+='
'+esc(a.name)+''+a.time+'
提示:越晚完成的科目,遗漏概率往往越高
'}return html} function renderTemplates(){var html='

任务模板 📋

配置周期规则,系统每天自动生成当日任务
';if(!state.templates.length)html+='
还没有任务模板
点下方按钮创建第一个
';state.templates.forEach(function(t){html+='
'+esc(t.title)+'
'+(t.category_name?'【'+esc(t.category_name)+'】 ':'')+TYPE[t.task_type]+(t.target_value?' · '+esc(t.target_value)+(t.unit?esc(t.unit):''):'')+'
每周:'+weekdaysCN(t.weekdays)+' | 从 '+String(t.start_date).slice(0,10)+' 起'+(t.end_date?' 至 '+String(t.end_date).slice(0,10):'(长期)')+'
+'+t.points+' 能量'+(t.description?'
要求:'+esc(t.description):'')+'
'});html+='';return html} function tplForm(t){var kids=state.children.map(function(c){return ''}).join('');var cats=state.categories.map(function(c){return ''}).join('');var wds=(t?t.weekdays:'1,2,3,4,5').split(',');var wchk=WD.map(function(w,i){return ''+w+''}).join('');return '
'} 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;await api('/parent/templates/'+id,{method:'DELETE'});await loadAll();render()} async function saveTpl(id){var wds=[];document.querySelectorAll('.overlay .chip.on').forEach(function(c){wds.push(String(WD.indexOf(c.textContent)+1))});var body={child_id:+document.getElementById('f_child').value,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,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)await api('/parent/templates/'+id,{method:'PUT',body:JSON.stringify(body)});else await api('/parent/templates',{method:'POST',body:JSON.stringify(body)});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='

分类管理 🏷️

学习 + 习惯,不限语数外
';state.categories.forEach(function(c){html+=''});html+='
'+esc(c.name)+'
';return html} async function addCat(){var n=document.getElementById('c_name').value.trim();if(!n)return;await api('/parent/categories',{method:'POST',body:JSON.stringify({name:n,color:document.getElementById('c_color').value})});await loadAll();render()} async function delCat(id,name){if(!confirm('删除分类「'+name+'」?使用它的模板将变为无分类。'))return;await api('/parent/categories/'+id,{method:'DELETE'});await loadAll();render()} function renderKids(){var html='

孩子管理 👦

儿童端选择孩子后进入
';state.children.forEach(function(c){html+='
'+esc(c.emoji)+' '+esc(c.name)+'
'});html+='
';return html} async function addKid(){var n=document.getElementById('k_name').value.trim();if(!n)return;await api('/parent/children',{method:'POST',body:JSON.stringify({name:n,emoji:document.getElementById('k_emoji').value||'🙂'})});await loadAll();render()} async function delKid(id,name){if(!confirm('删除「'+name+'」及其所有任务记录?此操作不可恢复!'))return;await api('/parent/children/'+id,{method:'DELETE'});await loadAll();render()} function renderSet(){return '

设置 ⚙️

· 儿童端地址:打开
· 任务每天首次访问时自动生成
· 历史任务保存快照,改模板不影响过去记录
· 数据存储:Memind Portal
'} async function chpw(){try{await api('/parent/password',{method:'POST',body:JSON.stringify({old:document.getElementById('p_old').value,new: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');state.pw='';render()} async function loadAll(){if(state.tab==='dash')state.stats=await api('/parent/stats');if(state.tab==='tpl'){state.templates=(await api('/parent/templates')).data;state.categories=(await api('/parent/categories')).data;state.children=(await api('/parent/children')).data}if(state.tab==='cat'){state.categories=(await api('/parent/categories')).data}if(state.tab==='kid'){state.children=(await api('/parent/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+'
'+tabs.map(function(t){return ''}).join('')+'
'} async function go(tab){state.tab=tab;try{await loadAll();render()}catch(e){if(e.message.indexOf('密码')>=0){state.pw='';localStorage.removeItem('learn_pw')}render()}} (async function(){if(state.pw){try{await loadAll()}catch(e){if(e.message.indexOf('密码')>=0){state.pw='';localStorage.removeItem('learn_pw')}}}render()})();`; } function wrapHtml(title, bodyExtra, script) { return ` ${title}
${title.includes('小助手') ? '
v1
' : '
'.replace('id="app"', '')}