ee8677cc6d
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>
127 lines
15 KiB
HTML
127 lines
15 KiB
HTML
<!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,'&').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>';
|
||
}
|
||
|
||
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> |