From f9e105acd092b987ac9a3d34190dacbec3b7fa40 Mon Sep 17 00:00:00 2001 From: john Date: Tue, 15 Sep 2026 17:17:18 +0800 Subject: [PATCH] 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 --- public/learn/index.html | 114 ++++++++++++++++++++++ public/learn/learning-assistant-data.js | 64 +++++++++++++ public/learn/parent.html | 116 +++++++++++++++++++++++ scripts/build-learn-standalone.py | 89 +++++++++++++++++ scripts/patch-learn-route-143.py | 42 ++++++++ server/portal-static-delivery-routes.mjs | 7 ++ 6 files changed, 432 insertions(+) create mode 100644 public/learn/index.html create mode 100644 public/learn/learning-assistant-data.js create mode 100644 public/learn/parent.html create mode 100644 scripts/build-learn-standalone.py create mode 100644 scripts/patch-learn-route-143.py diff --git a/public/learn/index.html b/public/learn/index.html new file mode 100644 index 0000000..d32a35f --- /dev/null +++ b/public/learn/index.html @@ -0,0 +1,114 @@ + + + + + +我的学习小助手 + + + + + + + + +
加载中…
+
+ + + + diff --git a/public/learn/learning-assistant-data.js b/public/learn/learning-assistant-data.js new file mode 100644 index 0000000..1955249 --- /dev/null +++ b/public/learn/learning-assistant-data.js @@ -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||dsb.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&&dse)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;i0)return "yellow";return 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=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 + + + + +家长后台 + + + + + + + + +
加载中…
+ + + + diff --git a/scripts/build-learn-standalone.py b/scripts/build-learn-standalone.py new file mode 100644 index 0000000..441f33f --- /dev/null +++ b/scripts/build-learn-standalone.py @@ -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'', html) + return m.group(1) if m else '' + + +def extract_app_script(html: str) -> str: + m = re.search(r'', 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''' + + + + +{title} + + + + + + + +{body} + + + +''' + + +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 = ''' +
加载中…
+
+''' + + parent_body = ''' +
加载中…
+''' + + (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() diff --git a/scripts/patch-learn-route-143.py b/scripts/patch-learn-route-143.py new file mode 100644 index 0000000..43e84a2 --- /dev/null +++ b/scripts/patch-learn-route-143.py @@ -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") diff --git a/server/portal-static-delivery-routes.mjs b/server/portal-static-delivery-routes.mjs index 4cb11b6..2824eb4 100644 --- a/server/portal-static-delivery-routes.mjs +++ b/server/portal-static-delivery-routes.mjs @@ -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',