Files
memind/public/learn/child.html
T
john 348e192800 feat(learn): add family learning assistant API routes and public pages
Wire /learn static delivery, parent/child portal routes, and Tang page build scripts for 103 deploy.

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

103 lines
23 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters
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><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}</style></head><body>
<div class="wrap" id="app"></div><div class="tabbar" id="tabbar"></div>
<div style="text-align:center;font-size:10px;color:#c3c9d6;padding:6px 0 76px">v2</div>
<script>
var FAMILY_SLUG=new URLSearchParams(location.search).get('f')||'tang';
var API_BASE='/api/learn/public/'+encodeURIComponent(FAMILY_SLUG);
var state={tab:'today',child:null,children:[],data:null,cal:null,grow:null};
function esc(s){return String(s==null?'':s).replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;')}
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='<div class="card"><h1>我是谁呀?</h1>';state.children.forEach(function(x){s+='<div class="chip" style="margin-top:12px;font-size:16px;padding:12px 20px" onclick="chooseChild('+x.id+')">'+esc(x.emoji)+' '+esc(x.name)+'</div>'});s+='</div>';return s}return c.id}
function chooseChild(id){localStorage.setItem('learn_child',String(id));location.reload()}
function showSwitcher(){var s='<div class="overlay" onclick="if(event.target===this)this.remove()"><div class="modal"><h3 style="font-size:17px">👤 切换学员</h3><div style="margin-top:8px">';state.children.forEach(function(x){s+='<div class="chip" style="width:100%;justify-content:center;margin:8px 0 0;font-size:16px;padding:12px 20px'+(x.id==state.child?';border-color:#3b5bfd;background:#e8edff;font-weight:700':'')+'" onclick="chooseChild('+x.id+')">'+esc(x.emoji)+' '+esc(x.name)+(x.id==state.child?' ✓':'')+'</div>'});s+='</div><button class="ghost" style="margin-top:16px;width:100%" onclick="this.closest(\'.overlay\').remove()">取消</button></div></div>';document.body.insertAdjacentHTML('beforeend',s)}
function hdrActions(){var cur=state.children.find(function(x){return x.id==state.child});return '<div style="display:flex;gap:8px;flex:none;padding-top:6px"><span class="hbtn" onclick="showSwitcher()">⇄ '+(cur?'切换学员':'学员')+'</span><a class="hbtn" href="' + 'parent.html?f=' + FAMILY_SLUG + '">家长端</a></div>'}
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='<header class="h"><div style="display:flex;justify-content:space-between;align-items:flex-start;gap:10px"><div><h1>'+greet()+''+esc(d.child.name)+' '+esc(d.child.emoji)+'</h1><div class="sub">'+d.date.replace(/-/g,'/')+' 周'+wdCN(dowOf(d.date))+'</div></div>'+hdrActions()+'</div></header><div class="card"><div class="row" style="align-items:baseline"><div><div class="big">'+d.done+' / '+d.total+'</div><div class="sub">今天已完成</div></div><div style="text-align:right"><div class="big" style="color:#f59e0b">⭐ '+d.energy+'</div><div class="sub">我的能量</div></div></div><div class="bar"><i style="width:'+p+'%"></i></div>'+(d.total&&d.done===d.total?'<div style="text-align:center;margin-top:10px;font-size:15px;font-weight:700;color:#16a34a">⭐ 今天全部完成,太棒啦!</div>':'')+'</div>';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+='<div class="card"><div class="empty">今天没有任务<br>好好休息,或找爸爸妈妈安排~</div></div>';order.forEach(function(k){s+='<div class="cat"><span class="cat-title" style="background:'+byCat[k].color+'1a;color:'+byCat[k].color+'">'+esc(k)+'</span><div class="card">';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+='<div class="task'+(t.status==='done'?' done':'')+'" onclick="doTask('+t.id+')"><div class="ck"'+(t.status==='pending'?' style="border-color:#e11d48"':'')+'>'+(t.status==='done'?'✓':'')+'</div><div class="info"><div class="t">'+esc(t.title)+'</div>'+(sub.length?'<div class="d">'+sub.join('<br>')+'</div>':'')+'</div><div class="pt">+'+t.points+'</div></div>'});s+='</div></div>'});if(d.streak>0)s+='<div class="card" style="text-align:center"><span style="font-size:15px">🔥 连续打卡 <b style="color:#f59e0b">'+d.streak+'</b> 天,继续保持!</span></div>';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='<header class="h"><div style="display:flex;justify-content:space-between;align-items:flex-start;gap:10px"><div><h1>我的日历 📅</h1><div class="sub">'+m.replace('-',' 年 ')+' 月</div></div>'+hdrActions()+'</div></header><div class="card"><div style="display:flex;justify-content:space-between;align-items:center"><button class="ghost" style="padding:6px 14px" onclick="calShift(-1)">← 上月</button><button class="ghost" style="padding:6px 14px" onclick="calShift(1)">下月 →</button></div><div class="cal">';['日','一','二','三','四','五','六'].forEach(function(w){s+='<div class="w">'+w+'</div>'});for(var i=0;i<lead;i++)s+='<div></div>';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='<i style="position:absolute;top:4px;right:5px;width:7px;height:7px;border-radius:50%;background:#e11d48;box-shadow:0 0 4px #e11d48"></i>';else if(st.status==='yellow')mark='<i style="position:absolute;top:4px;right:5px;width:7px;height:7px;border-radius:50%;background:#f59e0b"></i>';}}if(ds===today)cls+=' today';s+='<div class="'+cls+'" style="position:relative;'+(st&&st.status==='red'?'box-shadow:0 0 0 2px #e11d48 inset':'')+'" onclick="showDay(\''+ds+'\')">'+d+mark+'</div>'}s+='</div><div class="legend"><span><i class="dot" style="background:#16a34a"></i>全部完成</span><span><i class="dot" style="background:#f59e0b"></i>部分完成</span><span><i class="dot" style="background:#e11d48"></i>有遗漏(红点)</span><span><i class="dot" style="background:#c3c9d6"></i>无任务</span><div style="width:100%;text-align:center;margin-top:4px;color:#3b5bfd">点任意日期查看当天详情</div></div></div><div id="dayDetail"></div>';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<new Date(Date.now()+8*3600e3).toISOString().slice(0,10)?',有遗漏':'');stColor=ds<new Date(Date.now()+8*3600e3).toISOString().slice(0,10)?'#e11d48':'#d97706'}else{stTxt=ds<new Date(Date.now()+8*3600e3).toISOString().slice(0,10)?'全部未完成 ✕':'待完成';stColor=ds<new Date(Date.now()+8*3600e3).toISOString().slice(0,10)?'#e11d48':'#d97706'}}var s='<div class="card"><div class="row" style="align-items:center;margin-bottom:6px"><b style="font-size:15px;flex:1">'+ds.replace(/-/g,'/')+' 周'+wdCN(dowOf(ds))+(isToday?'(今天)':'')+'</b>'+(stTxt?'<span style="font-size:12px;font-weight:700;color:'+stColor+'">'+stTxt+'</span>':'')+'</div>';if(!j.tasks.length)s+='<div class="empty">那天没有任务</div>';j.tasks.forEach(function(t){var overdue=t.status==='pending'&&ds<new Date(Date.now()+8*3600e3).toISOString().slice(0,10);s+='<div class="task'+(t.status==='done'?' done':'')+'"><div class="ck"'+(overdue?' style="border-color:#e11d48;color:#e11d48;font-size:12px"':'')+'>'+(t.status==='done'?'✓':(overdue?'✕':''))+'</div><div class="info"><div class="t">'+esc(t.title)+'</div><div class="d"'+((overdue)?' style="color:#e11d48"':'')+'>'+esc(t.category_name||'')+(t.status==='done'?' · '+(t.completed_time||'')+' 完成':(overdue?' · 未完成':' · 待完成')+(t.value_submitted?' · '+esc(t.value_submitted)+(t.unit||''):''))+'</div></div></div>'});if(j.tasks.length)s+='<div class="sub" style="margin-top:10px">完成率:'+Math.round(done/j.tasks.length*100)+'%</div>';s+='</div>';el.innerHTML=s}
function renderGrow(){var g=state.grow;var s='<header class="h"><div style="display:flex;justify-content:space-between;align-items:flex-start;gap:10px"><div><h1>我的成长 🌱</h1></div>'+hdrActions()+'</div></header><div class="statcheck"><div class="sc"><div class="n" style="color:#f59e0b">⭐ '+g.energy+'</div><div class="l">我的能量</div></div><div class="sc"><div class="n" style="color:#ef4444">🔥 '+g.streak+'</div><div class="l">连续打卡</div></div><div class="sc"><div class="n">'+g.totalDone+'</div><div class="l">累计完成</div></div></div><div class="card"><div class="sub" style="margin-bottom:6px">💪 历史最长连续:<b>'+g.best+'</b> 天 全勤天数:<b>'+g.fullDays+'</b> 天</div></div><div class="card"><div class="t" style="font-weight:700;font-size:15px;margin-bottom:8px">近 30 天分类表现</div>';if(!g.byCategory.length)s+='<div class="empty">还没有记录,快去完成任务吧!</div>';g.byCategory.forEach(function(c){var p=c.total?Math.round(c.done/c.total*100):0;s+='<div style="margin:10px 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+'%"></i></div><span style="flex:0 0 40px;text-align:right;font-size:12px;color:#6b7280">'+p+'%</span></div></div>'});s+='</div>';return s}
function renderHist(){var h=state.hist;var today=new Date(Date.now()+8*3600e3).toISOString().slice(0,10);var s='<header class="h"><div style="display:flex;justify-content:space-between;align-items:flex-start;gap:10px"><div><h1>我的记录 📖</h1><div class="sub">每天的任务和完成情况,都记在这里'+(h.cycleStart?'<br>本期任务从 <b style="color:#3b5bfd">'+h.cycleStart.replace(/-/g,'/')+'</b> 开始':'')+'</div></div>'+hdrActions()+'</div></header>';if(!h.data.length)s+='<div class="card"><div class="empty">还没有记录<br>完成今天的任务,就会出现在这里哦</div></div>';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<today?'有遗漏':'待完成'));var stColor=day.status==='green'?'#16a34a':day.status==='red'?'#e11d48':'#d97706';var stBg=day.status==='green'?'#e7f8ee':day.status==='red'?'#ffe9ee':'#fff4dd';var open=day.date===today;var bodyId='hb'+idx;var dot=day.status==='green'?'🟢':(day.status==='red'?'🔴':'🟡');s+='<div class="card"><div class="row" style="align-items:center;cursor:pointer" onclick="toggleDay(\''+bodyId+'\',this)"><div style="flex:1;cursor:pointer"><span style="font-size:16px;font-weight:700;cursor:pointer">'+label+'</span> <span class="sub">'+w+' '+dot+'</span></div><span style="font-size:12px;font-weight:700;color:'+stColor+';background:'+stBg+';padding:3px 10px;border-radius:999px;cursor:pointer">'+stTxt+'</span><span style="flex:0 0 16px;text-align:right;color:#9ca3af;font-size:12px;cursor:pointer" class="arr">'+(open?'▾':'▸')+'</span></div><div id="'+bodyId+'" style="'+(open?'':'display:none')+';margin-top:4px">';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<today)sub.push('✕ 未完成');else sub.push('待完成');if(t.remark)sub.push('📝 '+esc(t.remark));s+='<div class="task'+(t.status==='done'?' done':'')+'"><div class="ck">'+(t.status==='done'?'✓':(t.status==='pending'&&day.date<today?'✕':''))+'</div><div class="info"><div class="t">'+esc(t.title)+'</div><div class="d">'+esc(t.category_name||'')+' · '+sub.join(' · ')+'</div></div></div>'});s+='</div></div>'});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 '<a href="javascript:void(0)" class="'+(state.tab===x[0]?'on':'')+'" onclick="go(\''+x[0]+'\')"><span class="ic">'+x[1]+'</span>'+x[2]+'</a>'}).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='<div class="card"><div class="empty">还没有小朋友的信息<br>请爸爸妈妈先去 <a href="' + 'parent.html?f=' + FAMILY_SLUG + '" style="color:#3b5bfd">家长后台</a> 添加</div></div>';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','<div class="overlay" onclick="if(event.target===this)this.remove()"><div class="modal"><h3 style="font-size:17px">✏️ '+esc(t.title)+'</h3><label>'+label+'</label><input id="vInput" inputmode="numeric" value="'+esc(def)+'" placeholder="填一个数字吧"><label>想说的悄悄话(选填)</label><textarea id="vRemark" rows="2" style="font-size:14px"></textarea><div class="row" style="margin-top:16px"><button class="ghost" onclick="this.closest(\'.overlay\').remove()">再等等</button><button onclick="submitVal('+id+')">✓ 完成打卡</button></div></div></div>');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='<div class="feed" onclick="this.remove()"><div class="em">🎉</div><div class="t1">完成啦!</div><div class="t2">今天已经完成 <b>'+j.done+'</b> / '+j.total+' 项<br>'+(j.allDone?'⭐ 太棒了,今天全部完成!':'再完成 '+(j.total-j.done)+' 项,今天就是全绿!')+'</div><div class="pt">+'+j.points+' 能量</div><div class="t2" style="margin-top:12px;font-size:11px;color:#c3c9d6">点一下继续</div></div>';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='<div class="card"><div class="empty">'+esc(e.message)+'</div></div>'}})();
</script></body></html>