348e192800
Wire /learn static delivery, parent/child portal routes, and Tang page build scripts for 103 deploy. Co-authored-by: Cursor <cursoragent@cursor.com>
136 lines
14 KiB
JavaScript
136 lines
14 KiB
JavaScript
#!/usr/bin/env node
|
||
import fs from 'node:fs';
|
||
import path from 'node:path';
|
||
import { fileURLToPath } from 'node:url';
|
||
|
||
const root = path.join(path.dirname(fileURLToPath(import.meta.url)), '..');
|
||
const outDir = path.join(root, 'public', 'learn');
|
||
|
||
const sharedCss = fs.readFileSync(path.join(root, 'scripts/build-tang-learning-assistant-pages.mjs'), 'utf8')
|
||
.match(/const sharedCss = `([\s\S]*?)`;/)[1];
|
||
|
||
function childPage() {
|
||
return `<!DOCTYPE html>
|
||
<html lang="zh-CN"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
|
||
<title>我的学习小助手</title><style>${sharedCss}</style></head><body>
|
||
<div class="wrap" id="app"></div><div class="tabbar" id="tabbar"></div>
|
||
<div style="text-align:center;font-size:10px;color:#c3c9d6;padding:6px 0 76px">v2</div>
|
||
<script>
|
||
var FAMILY_SLUG=new URLSearchParams(location.search).get('f')||'tang';
|
||
var API_BASE='/api/learn/public/'+encodeURIComponent(FAMILY_SLUG);
|
||
${childScriptBody()}
|
||
</script></body></html>`;
|
||
}
|
||
|
||
function childScriptBody() {
|
||
// childScript() 在 tang 构建里经 template literal 会把 \\' 变成 \';
|
||
// 这里直接读源码,需手动还原为 inline <script> 可用的转义。
|
||
return fs.readFileSync(path.join(root, 'scripts/build-tang-learning-assistant-pages.mjs'), 'utf8')
|
||
.match(/function childScript\(\) {\s*return `\s*([\s\S]*?)`;\s*}/)[1]
|
||
.replace(/var API_BASE='[^']*';\n?/g, '')
|
||
.replace(/learning-assistant-parent\.html/g, "' + 'parent.html?f=' + FAMILY_SLUG + '")
|
||
.replace(/\\\\'/g, "\\'");
|
||
}
|
||
|
||
function parentPage() {
|
||
return `<!DOCTYPE html>
|
||
<html lang="zh-CN"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
|
||
<title>家长后台 · 学习小助手</title><style>${sharedCss}body{padding-bottom:30px}.tabs{position:sticky;top:0;background:#f2f4fa;padding-top:2px;z-index:10}.pl{max-width:720px}</style></head><body>
|
||
<div class="wrap pl" id="app"></div>
|
||
<script>
|
||
var FAMILY_SLUG=new URLSearchParams(location.search).get('f')||'';
|
||
var API_BASE='/api/learn/me';
|
||
var state={tab:'dash',stats:null,templates:null,categories:null,children:null,family:null,role:null,invite:null};
|
||
function esc(s){return String(s==null?'':s).replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>')}
|
||
async function api(p,opt){opt=opt||{};opt.credentials='include';opt.headers=Object.assign({'Content-Type':'application/json'},opt.headers||{});var r=await fetch(API_BASE+p,opt);var j=await r.json().catch(function(){return{}});if(!r.ok)throw new Error(j.error||j.message||'请求失败');return j}
|
||
function pad(n){return n<10?'0'+n:''+n}
|
||
var WD=['一','二','三','四','五','六','日'];
|
||
function weekdaysCN(w){return w.split(',').map(function(x){return WD[+x-1]}).join('、')}
|
||
var TYPE={check:'完成型',count:'数量型',duration:'时长型',score:'成绩型'};
|
||
function childUrl(){return '/learn/child.html?f='+(state.family?state.family.slug:FAMILY_SLUG||'tang')}
|
||
function memindAuthUrl(){
|
||
return '/?return_to='+encodeURIComponent(location.pathname+location.search);
|
||
}
|
||
function renderAuth(){
|
||
return '<header class="h"><h1>家长后台 🔐</h1><div class="sub">登录 Memind 后管理孩子任务;支持邀请爸爸/妈妈共同管理</div></header>'
|
||
+'<div class="card"><p class="sub" style="line-height:1.8;margin-bottom:12px">请通过 Memind 首页登录或注册(不要用 /auth/login 直链)。登录成功后会自动回到本页。</p>'
|
||
+'<a href="'+memindAuthUrl()+'" style="display:block;text-align:center;padding:12px;border-radius:12px;background:#3b5bfd;color:#fff;text-decoration:none;font-weight:600;margin-bottom:10px">登录 / 注册 Memind</a>'
|
||
+'<div class="sub" style="text-align:center;margin-top:14px"><a href="'+childUrl()+'" style="color:#3b5bfd">← 返回小朋友页面</a></div></div>';
|
||
}
|
||
async function ensureFamily(){
|
||
var info=await api('/family');
|
||
state.family=info.family;state.role=info.role;return info;
|
||
}
|
||
function renderInvite(){
|
||
if(state.role!=='owner')return '';
|
||
var s='<div class="card"><div class="t" style="font-weight:700;font-size:15px;margin-bottom:8px">👨👩👧 邀请协家长</div><div class="sub" style="line-height:1.7">把链接发给爸爸/妈妈,对方注册 Memind 后打开即可加入同一家庭。</div>';
|
||
if(state.invite&&state.invite.joinUrl){s+='<div class="sub" style="margin-top:10px;word-break:break-all;background:#f8fafc;padding:10px;border-radius:10px">'+esc(state.invite.joinUrl)+'</div>'}
|
||
s+='<button style="width:100%;margin-top:12px" onclick="createInvite()">生成邀请链接</button></div>';
|
||
return s;
|
||
}
|
||
async function createInvite(){state.invite=await api('/invite',{method:'POST'});render()}
|
||
function renderSet(){
|
||
return '<header class="h"><h1>设置 ⚙️</h1></header>'
|
||
+renderInvite()
|
||
+'<div class="card"><div class="sub" style="line-height:1.8">· 儿童端:<a href="'+childUrl()+'" style="color:#3b5bfd">'+esc(childUrl())+'</a><br>· 家庭 slug:<b>'+esc(state.family.slug)+'</b><br>· 你的角色:<b>'+(state.role==='owner'?'主家长':'协家长')+'</b><br>· 成员:'+state.family.members.map(function(m){return esc(m.displayName||m.userId.slice(0,8))+'('+(m.role==='owner'?'主':'协')+')'}).join('、')+'</div></div>';
|
||
}
|
||
${parentDashboardScript()}
|
||
(async function(){try{var st=await fetch('/auth/status',{credentials:'include'}).then(function(r){return r.json()});if(!st.authenticated||!st.user){document.getElementById('app').innerHTML=renderAuth();return}await ensureFamily();await loadAll();render()}catch(e){document.getElementById('app').innerHTML='<div class="card"><div class="empty">'+esc(e.message)+'</div></div>'}})();
|
||
</script></body></html>`;
|
||
}
|
||
|
||
function parentDashboardScript() {
|
||
return `
|
||
function renderLogin(){return renderAuth()}
|
||
async function login(){await ensureFamily();await loadAll();render()}
|
||
function renderDash(){var s=state.stats;var tp=s.today.total?Math.round(s.today.done/s.today.total*100):0;var wp=s.week.total?Math.round(s.week.done/s.week.total*100):0;var st=s.children.length?s.children[0].streak:0;var html='<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>';s.trend.forEach(function(d){var p=d.total?Math.round(d.done/d.total*100):0;html+='<div class="card"><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>'});return html}
|
||
function renderTemplates(){var html='<header class="h"><h1>任务模板 📋</h1></header>';state.templates.forEach(function(t){html+='<div class="card"><b>'+esc(t.title)+'</b><div class="sub">+'+t.points+' 能量 · 每周 '+weekdaysCN(t.weekdays)+'</div></div>'});html+='<button style="width:100%;margin-top:14px" onclick="newTpl()">+ 新建任务模板</button>';return html}
|
||
function tplForm(t){var kids=state.children.map(function(c){return '<option value="'+c.id+'">'+esc(c.name)+'</option>'}).join('');var cats=state.categories.map(function(c){return '<option value="'+c.id+'">'+esc(c.name)+'</option>'}).join('');var wchk=WD.map(function(w,i){return '<span class="chip" onclick="this.classList.toggle(\\'on\\')">'+w+'</span>'}).join('');return '<div class="overlay" onclick="if(event.target===this)this.remove()"><div class="modal"><h3>'+(t?'编辑':'新建')+'任务模板</h3><select id="f_child">'+kids+'</select><input id="f_title" placeholder="任务名称"><select id="f_type"><option value="check">完成型</option><option value="count">数量型</option></select><div>'+wchk+'</div><button onclick="saveTpl('+(t?t.id:0)+')">保存</button></div></div>'}
|
||
function newTpl(){document.getElementById('app').insertAdjacentHTML('beforeend',tplForm(null))}
|
||
async function saveTpl(id){var wds=[];document.querySelectorAll('.overlay .chip.on').forEach(function(c,i){wds.push(String(i+1))});var body={child_id:+document.getElementById('f_child').value,title:document.getElementById('f_title').value,task_type:document.getElementById('f_type').value,weekdays:wds,start_date:new Date().toISOString().slice(0,10),points:10};if(id)await api('/parent/templates/'+id,{method:'PUT',body:JSON.stringify(body)});else await api('/parent/templates',{method:'POST',body:JSON.stringify(body)});document.querySelector('.overlay').remove();await loadAll();render()}
|
||
function renderCats(){var html='<header class="h"><h1>分类管理 🏷️</h1></header><div class="card"><table>';state.categories.forEach(function(c){html+='<tr><td>'+esc(c.name)+'</td></tr>'});html+='</table></div><div class="card"><input id="c_name" placeholder="分类名"><button onclick="addCat()">添加</button></div>';return html}
|
||
async function addCat(){var n=document.getElementById('c_name').value.trim();if(!n)return;await api('/parent/categories',{method:'POST',body:JSON.stringify({name:n,color:'#3b5bfd'})});await loadAll();render()}
|
||
function renderKids(){var html='<header class="h"><h1>孩子管理 👦</h1></header>';state.children.forEach(function(c){html+='<div class="card">'+esc(c.emoji)+' '+esc(c.name)+'</div>'});html+='<div class="card"><input id="k_name" placeholder="昵称"><button onclick="addKid()">添加</button></div>';return html}
|
||
async function addKid(){var n=document.getElementById('k_name').value.trim();if(!n)return;await api('/parent/children',{method:'POST',body:JSON.stringify({name:n,emoji:'🙂'})});await loadAll();render()}
|
||
async function loadAll(){if(state.tab==='dash')state.stats=await api('/parent/stats');if(state.tab==='tpl'){state.templates=(await api('/parent/templates')).data;state.categories=(await api('/parent/categories')).data;state.children=(await api('/parent/children')).data}if(state.tab==='cat'){state.categories=(await api('/parent/categories')).data}if(state.tab==='kid'){state.children=(await api('/parent/children')).data}}
|
||
function render(){var app=document.getElementById('app');if(!state.family){app.innerHTML=renderAuth();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;await loadAll();render()}
|
||
`;
|
||
}
|
||
|
||
function joinPage() {
|
||
return `<!DOCTYPE html><html lang="zh-CN"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>加入家庭</title>
|
||
<style>body{font-family:-apple-system,BlinkMacSystemFont,"PingFang SC",sans-serif;background:#f2f4fa;padding:24px}.card{max-width:420px;margin:40px auto;background:#fff;border-radius:16px;padding:24px;box-shadow:0 1px 3px rgba(0,0,0,.08)}button,a.btn{display:block;width:100%;text-align:center;padding:12px;border-radius:12px;border:none;font-weight:600;text-decoration:none;margin-top:10px}button{background:#3b5bfd;color:#fff}a.btn{background:#e8edff;color:#3b5bfd}</style></head><body>
|
||
<div class="card"><h1>加入学习家庭</h1><p id="msg" class="sub">正在处理邀请…</p><a class="btn" id="login" href="/" style="display:none">登录 / 注册 Memind</a></div>
|
||
<script>
|
||
var token=new URLSearchParams(location.search).get('token');
|
||
(async function(){
|
||
var msg=document.getElementById('msg');
|
||
if(!token){msg.textContent='邀请链接无效';return}
|
||
var st=await fetch('/auth/status',{credentials:'include'}).then(function(r){return r.json()});
|
||
if(!st.authenticated||!st.user){msg.textContent='请先登录 Memind,再加入家庭';document.getElementById('login').href='/?return_to='+encodeURIComponent(location.pathname+location.search);document.getElementById('login').style.display='block';return}
|
||
try{
|
||
var j=await fetch('/api/learn/me/join',{method:'POST',credentials:'include',headers:{'Content-Type':'application/json'},body:JSON.stringify({token:token})}).then(function(r){return r.json()});
|
||
if(j.error)throw new Error(j.error);
|
||
msg.textContent='已加入「'+j.family.name+'」,正在进入家长后台…';
|
||
location.href='/learn/parent.html?f='+encodeURIComponent(j.family.slug);
|
||
}catch(e){msg.textContent=e.message}
|
||
})();
|
||
</script></body></html>`;
|
||
}
|
||
|
||
function indexPage() {
|
||
return `<!DOCTYPE html><html lang="zh-CN"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>学习小助手</title>
|
||
<style>body{font-family:-apple-system,BlinkMacSystemFont,"PingFang SC",sans-serif;background:#f2f4fa;padding:24px;text-align:center}.card{max-width:480px;margin:40px auto;background:#fff;border-radius:16px;padding:28px}a{display:inline-block;margin:8px;padding:12px 18px;border-radius:999px;background:#3b5bfd;color:#fff;text-decoration:none;font-weight:600}</style></head><body>
|
||
<div class="card"><h1>学习小助手</h1><p>家长注册 Memind 后配置任务;孩子打开链接打卡。</p>
|
||
<a href="/learn/parent.html">家长后台</a><a href="/learn/child.html?f=tang" style="background:#e8edff;color:#3b5bfd">儿童端示例</a>
|
||
<a href="/?return_to=%2Flearn%2Fparent.html" style="background:#16a34a">登录 / 注册 Memind</a></div></body></html>`;
|
||
}
|
||
|
||
fs.mkdirSync(outDir, { recursive: true });
|
||
fs.writeFileSync(path.join(outDir, 'child.html'), childPage());
|
||
fs.writeFileSync(path.join(outDir, 'parent.html'), parentPage());
|
||
fs.writeFileSync(path.join(outDir, 'join.html'), joinPage());
|
||
fs.writeFileSync(path.join(outDir, 'index.html'), indexPage());
|
||
console.log(JSON.stringify({ ok: true, outDir }, null, 2));
|