Files
memind/public/learn/parent.html
T
john ee8677cc6d fix(learn): skip plaza share widget on learning assistant pages
Wire delivery path detection through portal static routes and strip baked share chrome from learn parent/child HTML.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-11 18:50:01 +08:00

127 lines
15 KiB
HTML
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh-CN"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
<title>家长后台 · 学习小助手</title><style>*{box-sizing:border-box;margin:0;padding:0;-webkit-tap-highlight-color:transparent}
body{font-family:-apple-system,BlinkMacSystemFont,"PingFang SC",sans-serif;background:#f2f4fa;color:#1c2333;min-height:100vh;padding-bottom:70px;padding-bottom:calc(70px + constant(safe-area-inset-bottom));padding-bottom:calc(70px + env(safe-area-inset-bottom))}
.wrap{max-width:560px;margin:0 auto;padding:16px 14px}
header.h{padding:16px 2px 4px}
h1{font-size:22px;font-weight:700}
.sub{font-size:13px;color:#6b7280;margin-top:3px}
.card{background:#fff;border-radius:16px;padding:16px;margin-top:12px;box-shadow:0 1px 3px rgba(28,35,51,.06)}
.big{font-size:26px;font-weight:700}
.bar{height:10px;background:#e8ebf2;border-radius:5px;overflow:hidden;margin-top:8px}
.bar i{display:block;height:100%;background:linear-gradient(90deg,#22c55e,#16a34a);border-radius:5px;transition:width .4s}
.cat{margin-top:14px}
.cat-title{font-size:13px;font-weight:700;padding:3px 10px;border-radius:999px;display:inline-flex;align-items:center;gap:5px}
.task{display:flex;align-items:center;gap:12px;padding:12px 2px;border-bottom:1px solid #f0f2f7;cursor:pointer}
.task:last-child{border-bottom:none}
.task .ck{width:26px;height:26px;border-radius:50%;border:2px solid #cbd5e1;flex:none;display:flex;align-items:center;justify-content:center;font-size:15px;color:#fff}
.task.done .ck{background:#16a34a;border-color:#16a34a}
.task .info{flex:1;min-width:0}
.task .t{font-size:15px;font-weight:600}
.task.done .t{color:#9ca3af;text-decoration:line-through}
.task .d{font-size:12px;color:#6b7280;margin-top:2px}
.task .pt{font-size:12px;color:#f59e0b;font-weight:700;flex:none}
.tabbar{position:fixed;left:0;right:0;bottom:0 !important;top:auto !important;background:#fff;border-top:1px solid #e5e8f0;display:flex;z-index:20;padding:6px 0 8px;padding:6px 0 calc(8px + constant(safe-area-inset-bottom));padding:6px 0 calc(8px + env(safe-area-inset-bottom))}
.overlay{top:0 !important;left:0 !important;right:0 !important;bottom:0 !important}
.tabbar a{flex:1;text-align:center;text-decoration:none;color:#9ca3af;font-size:11px;padding:4px 0}
.tabbar a.on{color:#3b5bfd;font-weight:700}
.tabbar .ic{font-size:20px;display:block}
button{font-family:inherit;border:none;cursor:pointer;border-radius:12px;font-size:14px;font-weight:600;padding:10px 16px;background:#3b5bfd;color:#fff}
button:active{transform:scale(.97)}
button.ghost{background:#e8edff;color:#3b5bfd}
button.danger{background:#ffe9ee;color:#e11d48}
button:disabled{opacity:.4;cursor:not-allowed}
input,select,textarea{font-family:inherit;width:100%;border:2px solid #e5e8f0;border-radius:10px;padding:10px 12px;font-size:15px;outline:none;background:#fff;color:#1c2333}
input:focus,select:focus,textarea:focus{border-color:#3b5bfd}
label{display:block;font-size:13px;font-weight:600;margin:12px 0 5px}
.overlay{position:fixed;inset:0;background:rgba(20,24,38,.5);z-index:40;display:flex;align-items:center;justify-content:center;padding:20px}
.modal{background:#fff;border-radius:18px;padding:24px;width:100%;max-width:360px;max-height:85vh;overflow:auto}
.statcheck{display:grid;grid-template-columns:repeat(3,1fr);gap:10px;margin-top:12px}
.sc{background:#fff;border-radius:14px;padding:13px 8px;text-align:center;box-shadow:0 1px 3px rgba(28,35,51,.06)}
.sc .n{font-size:20px;font-weight:700}
.sc .l{font-size:11px;color:#6b7280;margin-top:2px}
.cal{display:grid;grid-template-columns:repeat(7,1fr);gap:6px;margin-top:10px}
.cal .w{text-align:center;font-size:11px;color:#9ca3af;padding:4px 0}
.cal .cell{text-align:center;padding:7px 0;border-radius:10px;font-size:14px;position:relative}
.cal .cell.has{font-weight:700}
.cal .cell.g{background:#e7f8ee;color:#16a34a}.cal .cell.y{background:#fff4dd;color:#d97706}.cal .cell.r{background:#ffe9ee;color:#e11d48}.cal .cell.n{color:#c3c9d6}
.cal .today{outline:2px solid #3b5bfd}
.legend{display:flex;gap:12px;font-size:11px;color:#6b7280;margin-top:10px;flex-wrap:wrap}
.dot{display:inline-block;width:9px;height:9px;border-radius:50%;margin-right:4px;vertical-align:-1px}
.feed{position:fixed;left:50%;top:38%;transform:translate(-50%,-50%);background:#fff;border-radius:20px;padding:28px 30px;text-align:center;z-index:50;box-shadow:0 10px 40px rgba(28,35,51,.25);animation:pop .3s}
@keyframes pop{from{transform:translate(-50%,-50%) scale(.8);opacity:0}to{transform:translate(-50%,-50%) scale(1);opacity:1}}
.feed .em{font-size:44px}.feed .t1{font-size:18px;font-weight:700;margin-top:8px}.feed .t2{font-size:13px;color:#6b7280;margin-top:6px;line-height:1.6}
.feed .pt{color:#f59e0b;font-weight:700;margin-top:8px;font-size:15px}
table{width:100%;border-collapse:collapse;font-size:13px}
th,td{padding:8px 6px;border-bottom:1px solid #f0f2f7;text-align:left}
th{font-size:11px;color:#6b7280}
.row{display:flex;gap:10px;align-items:center}
.row>*{flex:1}
.pw{height:8px;background:#e8ebf2;border-radius:4px;overflow:hidden;min-width:60px}
.pw i{display:block;height:100%;background:#3b5bfd}
.chip{display:inline-flex;align-items:center;gap:6px;padding:6px 14px;border-radius:999px;border:2px solid #e5e8f0;font-size:13px;cursor:pointer;margin:0 8px 8px 0;background:#fff}
.chip.on{border-color:#3b5bfd;background:#e8edff;color:#3b5bfd;font-weight:700}
.hbtn{display:inline-flex;align-items:center;gap:4px;font-size:12px;font-weight:600;color:#3b5bfd;background:#e8edff;padding:6px 13px;border-radius:999px;text-decoration:none;cursor:pointer;flex:none;white-space:nowrap}
.hbtn:active{transform:scale(.95)}
.err{background:#ffe9ee;color:#e11d48;padding:10px;border-radius:10px;font-size:13px;margin-top:10px;display:none}
.tabs{display:flex;gap:8px;margin-top:14px}
.tabs button{flex:1;background:#e8ebf2;color:#6b7280}
.tabs button.on{background:#3b5bfd;color:#fff}
.empty{text-align:center;color:#9ca3af;font-size:13px;padding:18px 0;line-height:1.7}
[data-mindspace-public-share],[data-mindspace-wechat-dialog],.publication-share-fab,.publication-share-sheet{display:none!important;visibility:hidden!important}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,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;')}
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>';
}
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()}
(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>