Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f9e105acd0 |
@@ -0,0 +1,114 @@
|
||||
<!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="dcb1f3f0-db96-4dab-a009-7a9c393ebe77">
|
||||
<script>window.__MINDSPACE_PAGE_DATA__={"pageId":"dcb1f3f0-db96-4dab-a009-7a9c393ebe77","accessMode":"public"};</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}</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" id="app"><div class="card"><div class="empty">加载中…</div></div></div>
|
||||
<div class="tabbar" id="tabbar"></div>
|
||||
|
||||
<script>
|
||||
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,'<').replace(/>/g,'>')}
|
||||
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="/learn/parent.html">家长端</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 LearnStore.getDay(state.child,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 cycleTxt='';if(h.cycleStart){cycleTxt='<br>本期:'+h.cycleStart.replace(/-/g,'/')+' 起'+(h.cycleEnd?',至 '+h.cycleEnd.replace(/-/g,'/'):'(长期)')+' · 仅显示本期内有任务的日期'}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">本期任务完成情况'+(cycleTxt||'(请爸爸妈妈先配置本期任务)')+'</div></div>'+hdrActions()+'</div></header>';if(!h.cycleStart)s+='<div class="card"><div class="empty">还没有本期任务<br>请爸爸妈妈先在家长后台配置</div></div>';else 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 LearnStore.getChildren()).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 LearnStore.getToday(state.child);if(state.tab==='hist')state.hist=await LearnStore.getHistory(state.child);if(state.tab==='cal'&&!state.cal)state.cal=await LearnStore.getCalendar(state.child);if(state.tab==='grow')state.grow=await LearnStore.getGrowth(state.child)}
|
||||
function render(){var app=document.getElementById('app');if(!state.children.length){app.innerHTML='<div class="card"><div class="empty">还没有小朋友的信息<br>请爸爸妈妈先去 <a href="/learn/parent.html" 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 LearnStore.getCalendar(state.child,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 LearnStore.checkin(id,value,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 LearnStore.init();await load();render()}catch(e){document.getElementById('app').innerHTML='<div class="card"><div class="empty">'+esc(e.message)+'</div></div>'}})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,64 @@
|
||||
(function(global){
|
||||
"use strict";
|
||||
var client=null,cache={children:[],categories:[],templates:[],tasks:[],completions:[],settings:[]};
|
||||
var refreshAt=0,taskIndex=null,TOKEN_KEY="learn_pd_token",DATA_VER="20260915j";
|
||||
function cycleBounds(childId){var tpls=cache.templates.filter(function(t){return String(t.child_id)===String(childId)&&String(t.active)!=="0"});if(!tpls.length)return {cycleStart:"",cycleEnd:""};var starts=[],ends=[];tpls.forEach(function(t){var s=String(t.start_date||"").slice(0,10);if(s)starts.push(s);if(t.end_date){var e=String(t.end_date).slice(0,10);if(e)ends.push(e)}});starts.sort();ends.sort();return {cycleStart:starts[0]||"",cycleEnd:ends.length?ends[ends.length-1]:""}}
|
||||
function tasksForDateInCycle(childId,ds){var b=cycleBounds(childId);if(!b.cycleStart||ds<b.cycleStart)return [];if(b.cycleEnd&&ds>b.cycleEnd)return [];var ids={};cache.templates.forEach(function(t){if(String(t.child_id)===String(childId)&&String(t.active)!=="0")ids[String(t.id)]=true});return tasksForDate(childId,ds).filter(function(t){return t.template_id&&ids[String(t.template_id)]})}
|
||||
function pageId(){return global.__MINDSPACE_PAGE_DATA__&&global.__MINDSPACE_PAGE_DATA__.pageId||null}
|
||||
function parseWeekdays(raw){var s=String(raw||"1,2,3,4,5").trim();if(!s)return ["1","2","3","4","5"];if(s.charAt(0)==="{")return s.replace(/[{}\"]/g,"").split(",").filter(Boolean);return s.split(",").filter(Boolean)}
|
||||
async function rows(name,q,retry){try{var r=await client.listRows(name,q||{limit:500});return r.rows||[]}catch(e){if((retry||0)<1&&(e.status>=500||e.code==="page_data_failed"||!e.code))return rows(name,q,(retry||0)+1);throw e}}
|
||||
function liveRows(list){return (list||[]).filter(function(r){return r&&r.deleted_at==null&&String(r.active)!=="0"})}
|
||||
function todayStr(){return new Date(Date.now()+28800000).toISOString().slice(0,10)}
|
||||
function pad(n){return n<10?"0"+n:""+n}
|
||||
function dowUTC(ds){return new Date(Date.UTC(+ds.slice(0,4),+ds.slice(5,7)-1,+ds.slice(8,10))).getUTCDay()}
|
||||
function wdCN(d){return ["日","一","二","三","四","五","六"][d]}
|
||||
function addDays(ds,n){var d=new Date(ds+"T00:00:00Z");d.setUTCDate(d.getUTCDate()+n);return d.toISOString().slice(0,10)}
|
||||
function pageTokenKey(){return TOKEN_KEY+":"+(global.__MINDSPACE_PAGE_DATA__&&global.__MINDSPACE_PAGE_DATA__.pageId||"default")}
|
||||
function saveToken(t){try{if(t)sessionStorage.setItem(pageTokenKey(),t);else sessionStorage.removeItem(pageTokenKey())}catch(e){}}
|
||||
function loadToken(){try{return sessionStorage.getItem(pageTokenKey())||""}catch(e){return ""}}
|
||||
function rebuildTaskIndex(){taskIndex={};cache.tasks.forEach(function(t){var k=String(t.child_id)+"|"+String(t.task_date).slice(0,10);if(!taskIndex[k])taskIndex[k]=[];taskIndex[k].push(t)})}
|
||||
function invalidateTaskIndex(){taskIndex=null}
|
||||
async function refresh(force){if(!force&&client&&refreshAt&&Date.now()-refreshAt<15000)return;var packs=await Promise.all([rows("learn_children"),rows("learn_categories"),rows("learn_templates"),rows("learn_tasks"),rows("learn_completions")]);cache.children=liveRows(packs[0]);cache.categories=liveRows(packs[1]);cache.templates=liveRows(packs[2]);cache.tasks=liveRows(packs[3]);cache.completions=liveRows(packs[4]);cache.settings=[];invalidateTaskIndex();refreshAt=Date.now();rows("learn_settings").then(function(r){cache.settings=liveRows(r)}).catch(function(){})}
|
||||
function catById(id){return cache.categories.find(function(c){return String(c.id)===String(id)})||null}
|
||||
function childById(id){return cache.children.find(function(c){return String(c.id)===String(id)})||null}
|
||||
function tplActive(t,ds){if(String(t.active)==="0")return false;var s=String(t.start_date||"").slice(0,10);if(s&&ds<s)return false;var e=t.end_date?String(t.end_date).slice(0,10):"";if(e&&ds>e)return false;var wd=dowUTC(ds);var wds=parseWeekdays(t.weekdays);return wds.indexOf(String(wd===0?7:wd))>=0||wds.indexOf(String(wd))>=0}
|
||||
function compMap(childId){var m={};cache.completions.forEach(function(c){if(String(c.child_id)===String(childId))m[String(c.task_id)]=c});return m}
|
||||
function mergeTask(t,c){var x=Object.assign({},t);if(c){x.status="done";x.value_submitted=c.value||"";x.remark=c.remark||"";x.completed_time=c.completed_time||""}return x}
|
||||
function tasksForDate(childId,ds){if(!taskIndex)rebuildTaskIndex();var cm=compMap(childId);return (taskIndex[String(childId)+"|"+ds]||[]).map(function(t){return mergeTask(t,cm[String(t.id)])})}
|
||||
async function ensureTasks(childId,ds){var existing=tasksForDate(childId,ds);var hasTpl={};existing.forEach(function(t){if(t.template_id)hasTpl[String(t.template_id)]=true});var pending=[];for(var i=0;i<cache.templates.length;i++){var t=cache.templates[i];if(String(t.child_id)!==String(childId)||!tplActive(t,ds)||hasTpl[String(t.id)])continue;var cat=catById(t.category_id);pending.push({child_id:Number(childId),template_id:Number(t.id),task_date:ds,title:t.title,description:t.description||"",task_type:t.task_type||"check",target_value:t.target_value||"",unit:t.unit||"",category_id:t.category_id||null,category_name:cat?cat.name:"",category_color:cat?cat.color:"#6b7280",points:Number(t.points||10),status:"pending",value_submitted:"",remark:"",completed_time:""})}if(!pending.length)return existing;var created=await Promise.all(pending.map(function(body){return client.insertRow("learn_tasks",body).then(function(r){return r&&r.row?r.row:null}).catch(function(){return null})}));created.forEach(function(row){if(row)cache.tasks.push(row)});invalidateTaskIndex();return tasksForDate(childId,ds)}
|
||||
function dayStatus(tasks,ds){var today=todayStr();var done=tasks.filter(function(t){return t.status==="done"}).length;if(!tasks.length)return "none";if(done===tasks.length)return "green";if(done>0)return "yellow";return ds<today?"red":"yellow"}
|
||||
function streakFor(childId){var today=todayStr();var s=0;for(var i=0;i<120;i++){var ds=addDays(today,-i);var ts=tasksForDate(childId,ds);if(!ts.length)break;if(dayStatus(ts,ds)!=="green"){if(i===0&&dayStatus(ts,ds)==="yellow")continue;break}s++}return s}
|
||||
function energyFor(childId){var c=childById(childId);var base=c?Number(c.energy||0):0;var extra=cache.completions.filter(function(x){return String(x.child_id)===String(childId)}).reduce(function(a,x){return a+Number(x.points||0)},0);return base+extra}
|
||||
async function init(opts){opts=opts||{};var pid=pageId();if(!pid)throw new Error("页面未配置 pageId");client=MindSpacePageData.createClient({apiBase:"/api",pageId:pid});var saved=loadToken();if(saved){client.setToken(saved);try{await refresh(true);return client}catch(e){saveToken("");client.setToken(null)}}if(opts.password){var auth=await client.authenticate(opts.password);if(auth&&auth.token)saveToken(auth.token);await refresh(true)}else{await refresh(true)}return client}
|
||||
async function getChildren(){await refresh();return {data:cache.children.map(function(c){return {id:Number(c.id),name:c.name,emoji:c.emoji}})}}
|
||||
async function getToday(childId){var ds=todayStr();await ensureTasks(childId,ds);var tasks=tasksForDate(childId,ds);var c=childById(childId)||{name:"",emoji:"🙂"};var done=tasks.filter(function(t){return t.status==="done"}).length;return {date:ds,child:{name:c.name,emoji:c.emoji},done:done,total:tasks.length,energy:energyFor(childId),tasks:tasks.map(function(t){return Object.assign({},t,{id:Number(t.id),points:Number(t.points||0)})})}}
|
||||
async function getDay(childId,ds){await ensureTasks(childId,ds);return {tasks:tasksForDate(childId,ds).map(function(t){return Object.assign({},t,{id:Number(t.id)})})}}
|
||||
async function getHistory(childId){var today=todayStr();var b=cycleBounds(childId);if(!b.cycleStart)return {cycleStart:"",cycleEnd:"",data:[]};var endBound=b.cycleEnd&&b.cycleEnd<today?b.cycleEnd:today;var out=[];for(var ds=endBound;ds>=b.cycleStart;ds=addDays(ds,-1)){var tasks=tasksForDateInCycle(childId,ds);if(!tasks.length)continue;var done=tasks.filter(function(t){return t.status==="done"}).length;out.push({date:ds,done:done,total:tasks.length,status:dayStatus(tasks,ds),tasks:tasks})}return {cycleStart:b.cycleStart,cycleEnd:b.cycleEnd,data:out}}
|
||||
async function getCalendar(childId,month){var m=month||todayStr().slice(0,7);var y=+m.slice(0,4),mo=+m.slice(5,7);var daysIn=new Date(Date.UTC(y,mo,0)).getUTCDate();var days=[];for(var d=1;d<=daysIn;d++){var ds=y+"-"+pad(mo)+"-"+pad(d);var tasks=tasksForDate(childId,ds);var done=tasks.filter(function(t){return t.status==="done"}).length;days.push({date:ds,done:done,total:tasks.length,status:dayStatus(tasks,ds)})}return {month:m,days:days}}
|
||||
async function getGrowth(childId){var today=todayStr();var byCat={};var totalDone=0,best=0,fullDays=0,run=0;for(var i=89;i>=0;i--){var ds=addDays(today,-i);var tasks=tasksForDate(childId,ds);if(!tasks.length){run=0;continue}var st=dayStatus(tasks,ds);if(st==="green"){run++;fullDays++;if(run>best)best=run}else run=0;tasks.forEach(function(t){var k=t.category_name||"其他";if(!byCat[k])byCat[k]={name:k,done:0,total:0};byCat[k].total++;if(t.status==="done"){byCat[k].done++;totalDone++}})}return {energy:energyFor(childId),streak:streakFor(childId),best:best,fullDays:fullDays,totalDone:totalDone,byCategory:Object.keys(byCat).map(function(k){return byCat[k]})}}
|
||||
async function checkin(id,value,remark){var task=cache.tasks.find(function(t){return Number(t.id)===Number(id)});if(!task||task.status==="done")throw new Error("任务不存在或已完成");var ds=String(task.task_date).slice(0,10);var now=new Date(Date.now()+28800000);var ct=pad(now.getUTCHours())+":"+pad(now.getUTCMinutes());var pts=Number(task.points||10);await client.insertRow("learn_completions",{task_id:Number(id),child_id:Number(task.child_id),task_date:ds,value:value==null?"":String(value),remark:remark||"",completed_time:ct,points:pts});cache.completions.push({task_id:id,child_id:task.child_id,task_date:ds,value:value,remark:remark,completed_time:ct,points:pts});var todayTasks=tasksForDate(task.child_id,ds);var done=todayTasks.filter(function(t){return t.status==="done"}).length+1;var total=todayTasks.length;return {done:done,total:total,allDone:done>=total,points:pts}}
|
||||
async function parentStats(){await refresh();var today=todayStr();var weekStart=addDays(today,-6);var children=cache.children.map(function(c){return {name:c.name,emoji:c.emoji,streak:streakFor(c.id)}});var allToday={done:0,total:0},allWeek={done:0,total:0};var trend=[];for(var i=6;i>=0;i--){var ds=addDays(today,-i);var dDone=0,dTotal=0;for(var j=0;j<cache.children.length;j++){if(ds===today)await ensureTasks(cache.children[j].id,ds);var ts=tasksForDate(cache.children[j].id,ds);dDone+=ts.filter(function(t){return t.status==="done"}).length;dTotal+=ts.length}if(ds>=weekStart){allWeek.done+=dDone;allWeek.total+=dTotal}if(ds===today){allToday.done=dDone;allToday.total=dTotal}trend.push({d:ds,done:dDone,total:dTotal})}var missedMap={};for(var k=0;k<30;k++){var ds2=addDays(today,-k);for(var ci=0;ci<cache.children.length;ci++){tasksForDate(cache.children[ci].id,ds2).forEach(function(t){if(t.status!=="done"&&ds2<today){missedMap[t.title]=(missedMap[t.title]||0)+1}})}}var missed=Object.keys(missedMap).map(function(t){return {title:t,count:missedMap[t]}}).sort(function(a,b){return b.count-a.count}).slice(0,5);var catRate=[];cache.categories.forEach(function(cat){var done=0,total=0;for(var ci2=0;ci2<cache.children.length;ci2++){for(var k2=0;k2<30;k2++){tasksForDate(cache.children[ci2].id,addDays(today,-k2)).forEach(function(t){if(String(t.category_id)===String(cat.id)){total++;if(t.status==="done")done++}})}}catRate.push({name:cat.name,done:done,total:total})});return {today:allToday,week:allWeek,children:children,trend:trend,missed:missed,categoryRate:catRate,avgTime:[]}}
|
||||
async function parentList(kind){await refresh();if(kind==="templates"){return {data:cache.templates.map(function(t){var cat=catById(t.category_id);return Object.assign({},t,{id:Number(t.id),child_id:Number(t.child_id),category_id:t.category_id?Number(t.category_id):null,category_name:cat?cat.name:"",points:Number(t.points||0)})})}}if(kind==="categories")return {data:cache.categories.map(function(c){return {id:Number(c.id),name:c.name,color:c.color}})};if(kind==="children")return {data:cache.children.map(function(c){return {id:Number(c.id),name:c.name,emoji:c.emoji}})};return {data:[]}}
|
||||
async function parentInsert(kind,body){var r;if(kind==="children")r=await client.insertRow("learn_children",{name:body.name,emoji:body.emoji||"🙂",energy:0});else if(kind==="categories")r=await client.insertRow("learn_categories",{name:body.name,color:body.color||"#3b5bfd"});else if(kind==="templates")r=await client.insertRow("learn_templates",{child_id:body.child_id,category_id:body.category_id||null,title:body.title,description:body.description||"",task_type:body.task_type||"check",target_value:body.target_value||"",unit:body.unit||"",weekdays:body.weekdays,start_date:body.start_date,end_date:body.end_date||null,points:body.points||10,active:1});await refresh(true);return r}
|
||||
async function parentInsertMany(kind,bodies){var out=[];for(var i=0;i<bodies.length;i++){if(kind==="templates")out.push(await client.insertRow("learn_templates",{child_id:bodies[i].child_id,category_id:bodies[i].category_id||null,title:bodies[i].title,description:bodies[i].description||"",task_type:bodies[i].task_type||"check",target_value:bodies[i].target_value||"",unit:bodies[i].unit||"",weekdays:bodies[i].weekdays,start_date:bodies[i].start_date,end_date:bodies[i].end_date||null,points:bodies[i].points||10,active:1}))}await refresh(true);return out}
|
||||
async function parentUpdate(kind,id,body){var ds=kind==="templates"?"learn_templates":kind==="categories"?"learn_categories":"learn_children";var r=await client.updateRow(ds,id,body);await refresh(true);return r}
|
||||
async function parentDelete(kind,id){
|
||||
if(kind==="templates"){await client.updateRow("learn_templates",id,{active:0});await refresh(true);return}
|
||||
if(kind==="children"){
|
||||
var cid=String(id);
|
||||
var ops=[];
|
||||
cache.completions.filter(function(x){return String(x.child_id)===cid}).forEach(function(x){ops.push(client.deleteRow("learn_completions",x.id).catch(function(){}))});
|
||||
cache.tasks.filter(function(x){return String(x.child_id)===cid}).forEach(function(x){ops.push(client.deleteRow("learn_tasks",x.id).catch(function(){}))});
|
||||
cache.templates.filter(function(x){return String(x.child_id)===cid}).forEach(function(x){ops.push(client.deleteRow("learn_templates",x.id).catch(function(){}))});
|
||||
if(ops.length)await Promise.all(ops);
|
||||
await client.deleteRow("learn_children",id);
|
||||
await refresh(true);return
|
||||
}
|
||||
await client.deleteRow(kind==="categories"?"learn_categories":"learn_tasks",id);
|
||||
await refresh(true)
|
||||
}
|
||||
async function parentPassword(oldPw,newPw){throw new Error("热修版暂不支持在线改密,默认口令 88888888")}
|
||||
async function verifyPassword(pw){clearAuth();try{await init({password:pw})}catch(e){if(e&&e.code==="rate_limited")throw e;if(e&&e.code==="password_invalid")throw new Error("密码错误");throw e}}
|
||||
function clearAuth(){saveToken("");if(client)client.setToken(null);refreshAt=0;invalidateTaskIndex()}
|
||||
global.LearnStore={init:init,refresh:refresh,clearAuth:clearAuth,getChildren:getChildren,getToday:getToday,getDay:getDay,getHistory:getHistory,getCalendar:getCalendar,getGrowth:getGrowth,checkin:checkin,parentStats:parentStats,parentList:parentList,parentInsert:parentInsert,parentInsertMany:parentInsertMany,parentUpdate:parentUpdate,parentDelete:parentDelete,parentPassword:parentPassword,verifyPassword:verifyPassword,todayStr:todayStr,wdCN:wdCN,dowOf:dowUTC,addDays:addDays,parseWeekdays:parseWeekdays,DATA_VER:DATA_VER};
|
||||
})(window);
|
||||
@@ -0,0 +1,116 @@
|
||||
<!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,'&').replace(/</g,'<').replace(/>/g,'>')}
|
||||
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>
|
||||
@@ -0,0 +1,89 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build lightweight /learn/* pages without MindSpace injection."""
|
||||
import re
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
SRC = ROOT / 'MindSpace/a70ff537-8908-486e-9b6c-042e07cc25db/public'
|
||||
OUT = ROOT / 'public/learn'
|
||||
VER = '20260915j'
|
||||
|
||||
CHILD_PAGE = 'dcb1f3f0-db96-4dab-a009-7a9c393ebe77'
|
||||
PARENT_PAGE = 'f9c80392-9417-46fd-af89-a91a239d3bfc'
|
||||
|
||||
|
||||
def extract_style(html: str) -> str:
|
||||
m = re.search(r'<style>([\s\S]*?)</style>', html)
|
||||
return m.group(1) if m else ''
|
||||
|
||||
|
||||
def extract_app_script(html: str) -> str:
|
||||
m = re.search(r'<script>\s*(var state=[\s\S]*?\)\(\);)\s*</script>', html)
|
||||
if not m:
|
||||
raise ValueError('app script not found')
|
||||
return m.group(1)
|
||||
|
||||
|
||||
def shell(title: str, style: str, page_id: str, access: str, body: str, script: str) -> str:
|
||||
access_json = '{"pageId":"%s","accessMode":"%s"}' % (page_id, access)
|
||||
return f'''<!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}</title>
|
||||
<meta name="mindspace-page-data-page-id" content="{page_id}">
|
||||
<script>window.__MINDSPACE_PAGE_DATA__={access_json};</script>
|
||||
<style>{style}</style>
|
||||
<script src="/assets/page-data-client.js?v={VER}"></script>
|
||||
<script src="/learn/learning-assistant-data.js?v={VER}"></script>
|
||||
<link rel="icon" type="image/png" href="/brand/tkmind-icon.png">
|
||||
</head>
|
||||
{body}
|
||||
<script>
|
||||
{script}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
'''
|
||||
|
||||
|
||||
def main():
|
||||
OUT.mkdir(parents=True, exist_ok=True)
|
||||
child_src = (SRC / 'learning-assistant.html').read_text(encoding='utf-8')
|
||||
parent_src = (SRC / 'learning-assistant-parent.html').read_text(encoding='utf-8')
|
||||
child_style = extract_style(child_src)
|
||||
parent_style = extract_style(parent_src)
|
||||
child_script = extract_app_script(child_src)
|
||||
parent_script = extract_app_script(parent_src)
|
||||
|
||||
child_script = child_script.replace('learning-assistant-parent.html', '/learn/parent.html')
|
||||
parent_script = parent_script.replace('learning-assistant.html', '/learn/')
|
||||
|
||||
child_body = '''<body>
|
||||
<div class="wrap" id="app"><div class="card"><div class="empty">加载中…</div></div></div>
|
||||
<div class="tabbar" id="tabbar"></div>
|
||||
'''
|
||||
|
||||
parent_body = '''<body>
|
||||
<div class="wrap pl" id="app"><div class="card"><div class="empty">加载中…</div></div></div>
|
||||
'''
|
||||
|
||||
(OUT / 'index.html').write_text(
|
||||
shell('我的学习小助手', child_style, CHILD_PAGE, 'public', child_body, child_script),
|
||||
encoding='utf-8',
|
||||
)
|
||||
(OUT / 'parent.html').write_text(
|
||||
shell('家长后台', parent_style, PARENT_PAGE, 'password', parent_body, parent_script),
|
||||
encoding='utf-8',
|
||||
)
|
||||
data_js = (SRC / 'learning-assistant-data.js').read_text(encoding='utf-8')
|
||||
data_js = re.sub(r'DATA_VER="[^"]+"', f'DATA_VER="{VER}"', data_js)
|
||||
(OUT / 'learning-assistant-data.js').write_text(data_js, encoding='utf-8')
|
||||
print('built', OUT)
|
||||
print(' index.html', (OUT / 'index.html').stat().st_size, 'bytes')
|
||||
print(' parent.html', (OUT / 'parent.html').stat().st_size, 'bytes')
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,42 @@
|
||||
#!/usr/bin/env python3
|
||||
from pathlib import Path
|
||||
|
||||
p = Path("/Users/john/Project/Memind/server.mjs")
|
||||
text = p.read_text()
|
||||
if '"/learn"' in text and "public/learn" in text:
|
||||
print("ALREADY_PATCHED")
|
||||
raise SystemExit(0)
|
||||
|
||||
needle = """ app2.use(
|
||||
"/assets",
|
||||
staticMiddleware(
|
||||
pathApi.join(h5Root, "public/assets"),
|
||||
{ maxAge: "1d" }
|
||||
)
|
||||
);
|
||||
app2.get(
|
||||
"/dev/wechat-share-preview","""
|
||||
|
||||
insert = """ app2.use(
|
||||
"/assets",
|
||||
staticMiddleware(
|
||||
pathApi.join(h5Root, "public/assets"),
|
||||
{ maxAge: "1d" }
|
||||
)
|
||||
);
|
||||
app2.use(
|
||||
"/learn",
|
||||
staticMiddleware(
|
||||
pathApi.join(h5Root, "public/learn"),
|
||||
{ maxAge: "5m", index: "index.html" }
|
||||
)
|
||||
);
|
||||
app2.get(
|
||||
"/dev/wechat-share-preview","""
|
||||
|
||||
if needle not in text:
|
||||
print("NEEDLE_NOT_FOUND")
|
||||
raise SystemExit(1)
|
||||
|
||||
p.write_text(text.replace(needle, insert, 1))
|
||||
print("PATCHED_OK")
|
||||
@@ -161,6 +161,13 @@ export function attachPortalStaticDeliveryRoutes({
|
||||
{ maxAge: '1d' },
|
||||
),
|
||||
);
|
||||
app.use(
|
||||
'/learn',
|
||||
staticMiddleware(
|
||||
pathApi.join(h5Root, 'public/learn'),
|
||||
{ maxAge: '5m', index: 'index.html' },
|
||||
),
|
||||
);
|
||||
|
||||
app.get(
|
||||
'/dev/wechat-share-preview',
|
||||
|
||||
Reference in New Issue
Block a user