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

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

65 lines
15 KiB
JavaScript

(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);