This commit is contained in:
2026-08-08 15:49:06 +08:00
parent e7dce1680a
commit 0a7207fa0e
76 changed files with 22779 additions and 0 deletions
+1509
View File
File diff suppressed because it is too large Load Diff
+1263
View File
File diff suppressed because it is too large Load Diff
+1041
View File
File diff suppressed because it is too large Load Diff
+642
View File
@@ -0,0 +1,642 @@
const API_BASE='',USER_ID=localStorage.getItem('mb_user_id')||('mb_'+Math.random().toString(36).slice(2,10));
localStorage.setItem('mb_user_id',USER_ID);
let CP='home',allV=[],allK=[],kT=[],vD=[],cD='',cT='',sD=null,sVN={currentNode:null,stats:{shuli:10,xiayi:10,xinzhi:10},fav:{}},simD=null,simS={currentNode:null,stats:{gpa:3,interest:0,skill:0}},favC={},chatCtx={},tt=null;
let cardData=null,cardCache=null,_gachaBusy=false;
let cardState={owned:[],draws:0,lastDaily:'',knowledgeRead:0,drawHistory:[]};
let _lang=localStorage.getItem('mb_lang')||'zh';
const I18N={zh:{home:'首页',discover:'发现',toolbox:'工具箱',knowledge:'知识卡片',story:'葫芦侠行',simulator:'专业模拟器',favorites:'我的收藏',profile:'个人中心',search:'搜索内容...',guest:'游客用户',
heroTitle:'葫芦数学 · 用玩的方式打开数学',heroSub:'数学不是公式,是你理解世界的方式',heroCTA:'开始探索 →',
exploreTitle:'探索数学的万千可能',tryTitle:'试试这些',
mbtiName:'MathBTI 数学人格',mbtiDesc:'12题发现你的数学人格,领养专属数学小人 →',
toolboxName:'数学工具箱',toolboxDesc:'口算竞赛 · 计算器 · 符号查询 · 函数绘图 →',
storyName:'葫芦侠行',storyDesc:'北宋数学武侠 · 8章93节点 · 8种结局 →',
simName:'数学人生',simDesc:'模拟大学数学系4年,选择方向走向不同结局 →',
videoName:'发现视频',videoDesc:'12个学科分区',cardName:'知识卡片',cardDesc:'8个数学话题',
aiName:'AI 助教',aiDesc:'随时解答数学疑问',
petTip:'每天使用工具箱和模拟器获取经验升级',
dailyFactPrefix:'💡 数学冷知识:',
discoverGreet:'Hello there. 这里收集了来自各专业志愿者的数学科普视频。',
filterAll:'全部',noVideo:'暂无视频',noCard:'暂无卡片',noResult:'无结果',
loadFailVideo:'视频加载失败',loadFailCard:'卡片加载失败',loadFailProfile:'加载失败',
loadMore:'下拉加载更多…',loadEnd:'— 已经到底啦 · 共 ',loadEndSuf:' 个视频 —',
close:'关闭',save:'保存',saveOK:'保存成功',saveFail:'保存失败',
toolboxHeader:'工具箱',toolboxSub:'动手做数学——口算、计算、查询、绘图',
toolMathrace:'口算竞赛',toolMathraceDesc:'限时60秒,挑战你的心算速度',
toolCalc:'计算器',toolCalcDesc:'支持加减乘除、幂运算、三角函数',
toolSym:'数学符号查询',toolSymDesc:'点击符号查看含义和LaTeX写法',
toolGraph:'函数图形绘图',toolGraphDesc:'输入 f(x),实时绘制函数曲线',
knowledgeHeader:'知识卡片',knowledgeSub:'轻量数学发现,每天一个新概念',
storyHeader:'葫芦侠行',storySub:'北宋数学武侠互动文字冒险 · 8章93节点 · 8+种结局',
storyLoadFail:'故事加载失败',storyNodeNotFound:'节点未找到',
simHeader:'专业模拟器',simSub:'模拟大学数学系4年 · 选择方向 · 走向不同结局',
simLoadFail:'模拟器加载失败',simReset:'重新开始',
favHeader:'我的收藏',favSub:'已收藏的视频和知识卡片',
favEmpty:'暂无收藏',favEmptyHint:'在发现页点击 🤍 即可收藏',
profileHeader:'个人中心',profileEdit:'编辑资料',
profileAvatar:'头像:',profileNick:'昵称:',
profileWatchCount:'观看次数',profileFavCount:'收藏数量',
aiHeader:'AI 数学助教',aiWelcome:'你好!我是葫芦数学的AI助教 👋<br><br>试试问我:学数学对学医有什么用?',
aiPlaceholder:'输入问题...',aiOffline:'抱歉,AI暂时无法回复',aiNetworkErr:'网络异常,请稍后重试',
calcErr:'表达式有误',calcPlaceholder:'输入表达式 如 2+3*4',
graphPlaceholder:'如 Math.sin(x) 或 x*x',graphBtn:'绘制',
mrTitle:'准备好了吗?',mrPlaceholder:'输入答案',mrTimeout:'⏰ 时间到!得分:',mrCorrect:'正确',mrRight:'✅ 正确!+10分',
symDetail:'👆 点击符号查看详情',
mbtiFloat:'测测你的数学人格',
aiAskBtn:'问问AI',aiAskSuffix:'是什么?',
petLevelUp:'升级啦!Lv.{n}',
staticHeroTitle:'葫芦数学 · 用玩的方式打开数学',staticHeroSub:'数学不是公式,是你理解世界的方式',staticHeroCTA:'开始探索 →',
staticExploreTitle:'探索数学的万千可能',staticTryTitle:'试试这些',
staticDiscoverGreet:'Hello there. 这里收集了来自各专业志愿者的数学科普视频。',
staticToolboxTitle:'🧰 工具箱',staticToolboxSub:'动手做数学——口算、计算、查询、绘图',
staticToolMathrace:'口算竞赛',staticToolMathraceDesc:'限时60秒,挑战你的心算速度',
staticToolCalc:'计算器',staticToolCalcDesc:'支持加减乘除、幂运算、三角函数',
staticToolSym:'数学符号查询',staticToolSymDesc:'点击符号查看含义和LaTeX写法',
staticToolGraph:'函数图形绘图',staticToolGraphDesc:'输入 f(x),实时绘制函数曲线',
staticKnowledgeTitle:'📚 知识卡片',staticKnowledgeSub:'轻量数学发现,每天一个新概念',
staticStoryTitle:'⚔️ 葫芦侠行',staticStorySub:'北宋数学武侠互动文字冒险 · 8章93节点 · 8+种结局',
staticSimTitle:'🎮 专业模拟器',staticSimSub:'模拟大学数学系4年 · 选择方向 · 走向不同结局',
staticFavTitle:'❤️ 我的收藏',staticFavSub:'已收藏的视频和知识卡片',
staticProfileTitle:'👤 个人中心',
staticAIToggleTitle:'AI数学助教',staticAIHeader:'💬 AI 数学助教',
staticAIWelcome:'你好!我是葫芦数学的AI助教 👋<br><br>试试问我:学数学对学医有什么用?',
staticAIPlaceholder:'输入问题...',staticMbtiTooltip:'测测你的数学人格'},
en:{home:'Home',discover:'Discover',toolbox:'Toolbox',knowledge:'Cards',story:'HuLu Quest',simulator:'Simulator',favorites:'Favorites',profile:'Profile',search:'Search...',guest:'Guest',
heroTitle:'Hulu Math · Play your way into math',heroSub:'Math isn\'t formulas. It\'s how you make sense of things.',heroCTA:'Start Exploring →',
exploreTitle:'Explore Math',tryTitle:'Try These',
mbtiName:'MathBTI Personality Test',mbtiDesc:'12 questions. One math soul. Meet your sprite. →',
toolboxName:'Math Toolbox',toolboxDesc:'Mental Math · Calculator · Symbol Lookup · Graph →',
storyName:'HuLu Quest',storyDesc:'A Song Dynasty math wuxia · 8 chapters · 8+ endings →',
simName:'Math Life',simDesc:'Live four years as a math major. Choose your path. →',
videoName:'Discover Videos',videoDesc:'12 subject areas',cardName:'Knowledge Cards',cardDesc:'8 math topics',
aiName:'AI Tutor',aiDesc:'Ask anything. No judgment.',
petTip:'Come back daily to earn EXP and level up your sprite.',
dailyFactPrefix:'💡 Math Trivia: ',
discoverGreet:'A hand-picked collection of math videos, curated by volunteers from across the sciences.',
filterAll:'All',noVideo:'No videos yet',noCard:'No cards yet',noResult:'No results',
loadFailVideo:'Couldn\'t load videos',loadFailCard:'Couldn\'t load cards',loadFailProfile:'Couldn\'t load',
loadMore:'Scroll for more…',loadEnd:'— That\'s all of them · ',loadEndSuf:' videos —',
close:'Close',save:'Save',saveOK:'Saved',saveFail:'Couldn\'t save',
toolboxHeader:'Toolbox',toolboxSub:'Get your hands dirty — race the clock, crunch numbers, explore symbols, draw curves',
toolMathrace:'Mental Math',toolMathraceDesc:'60 seconds. How high can you score?',
toolCalc:'Calculator',toolCalcDesc:'Quick arithmetic, powers, trig — whatever you need.',
toolSym:'Symbol Lookup',toolSymDesc:'Tap a symbol to see what it means and how to write it in LaTeX.',
toolGraph:'Function Grapher',toolGraphDesc:'Type any f(x) and watch its curve appear in real time.',
knowledgeHeader:'Knowledge Cards',knowledgeSub:'One idea at a time. Bite-sized math, built for curiosity.',
storyHeader:'HuLu Quest',storySub:'A math-fueled wuxia set in the Song Dynasty. 8 chapters. 8+ endings.',
storyLoadFail:'Couldn\'t load the story',storyNodeNotFound:'Node not found',
simHeader:'Math Major Simulator',simSub:'Four years. Real choices. Multiple endings. What kind of mathematician will you become?',
simLoadFail:'Couldn\'t load the simulator',simReset:'Start fresh',
favHeader:'My Favorites',favSub:'Your saved videos and cards',
favEmpty:'Nothing saved yet',favEmptyHint:'Tap 🤍 on any video or card to save it here.',
profileHeader:'Profile',profileEdit:'Edit Profile',
profileAvatar:'Avatar:',profileNick:'Nickname:',
profileWatchCount:'Videos watched',profileFavCount:'Favorites',
aiHeader:'AI Math Tutor',aiWelcome:'Hey! I\'m your Hulu Math AI tutor 👋<br><br>Try asking: "Why do we even need calculus?"',
aiPlaceholder:'Ask anything...',aiOffline:'The AI is taking a quick break. Try again in a moment.',aiNetworkErr:'Looks like you\'re offline. Check your connection and try again.',
calcErr:'That doesn\'t look right',calcPlaceholder:'e.g. 2+3*4',
graphPlaceholder:'e.g. Math.sin(x) or x*x',graphBtn:'Plot',
mrTitle:'Ready?',mrPlaceholder:'Type your answer',mrTimeout:'⏰ Time\'s up! You scored ',mrCorrect:'right',mrRight:'✅ Correct! +10',
symDetail:'👆 Tap any symbol to learn more',
mbtiFloat:'Discover your Math Personality',
aiAskBtn:'Ask AI',aiAskSuffix:'?',
petLevelUp:'Level up! Now Lv.{n}',
staticHeroTitle:'Hulu Math · Play your way into math',staticHeroSub:'Math isn\'t formulas. It\'s how you make sense of things.',staticHeroCTA:'Start Exploring →',
staticExploreTitle:'Explore Math',staticTryTitle:'Try These',
staticDiscoverGreet:'A hand-picked collection of math videos, curated by volunteers from across the sciences.',
staticToolboxTitle:'🧰 Toolbox',staticToolboxSub:'Get your hands dirty — race the clock, crunch numbers, explore symbols, draw curves',
staticToolMathrace:'Mental Math',staticToolMathraceDesc:'60 seconds. How high can you score?',
staticToolCalc:'Calculator',staticToolCalcDesc:'Quick arithmetic, powers, trig — whatever you need.',
staticToolSym:'Symbol Lookup',staticToolSymDesc:'Tap a symbol to see what it means and how to write it in LaTeX.',
staticToolGraph:'Function Grapher',staticToolGraphDesc:'Type any f(x) and watch its curve appear in real time.',
staticKnowledgeTitle:'📚 Knowledge Cards',staticKnowledgeSub:'One idea at a time. Bite-sized math, built for curiosity.',
staticStoryTitle:'⚔️ HuLu Quest',staticStorySub:'A math-fueled wuxia set in the Song Dynasty. 8 chapters. 8+ endings.',
staticSimTitle:'🎮 Math Major Simulator',staticSimSub:'Four years. Real choices. Multiple endings. What kind of mathematician will you become?',
staticFavTitle:'❤️ My Favorites',staticFavSub:'Your saved videos and cards',
staticProfileTitle:'👤 Profile',
staticAIToggleTitle:'AI Math Tutor',staticAIHeader:'💬 AI Math Tutor',
staticAIWelcome:'Hey! I\'m your Hulu Math AI tutor 👋<br><br>Try asking: "Why do we even need calculus?"',
staticAIPlaceholder:'Ask anything...',
staticMbtiTooltip:'Discover your Math Personality'}};
function t(k){const d=I18N[_lang];return (d&&d[k])||(I18N.zh[k]||k);}
function rI18n(){
document.querySelectorAll('[data-i18n]').forEach(el=>{
const k=el.getAttribute('data-i18n');
const v=t(k);
if(el.tagName==='INPUT'){el.placeholder=v}
else if(el.hasAttribute('title')&&el.getAttribute('data-i18n-type')==='title'){el.title=v}
else{el.innerHTML=v}
});
}
function switchLang(l){
_lang=l;localStorage.setItem('mb_lang',l);
document.querySelectorAll('.lang-btn').forEach(b=>b.classList.toggle('active',b.dataset.lang===l));
document.getElementById('globalSearch').placeholder=t('search');
rI18n();
navigateTo(CP);
api('/api/user/profile?user_id='+USER_ID).then(r=>{if(r.code===0&&r.data){document.getElementById('sidebarName').textContent=r.data.nickname||t('guest')}}).catch(()=>{});
}
function toggleSidebar(){document.getElementById('sidebar').classList.toggle('open');document.getElementById('sidebarOverlay').classList.toggle('active')}
function closeSidebar(){document.getElementById('sidebar').classList.remove('open');document.getElementById('sidebarOverlay').classList.remove('active')}
let searchTimeout;
function handleGlobalSearch(){
clearTimeout(searchTimeout);
searchTimeout=setTimeout(()=>{
const q=document.getElementById('globalSearch').value.trim().toLowerCase();
const el=document.getElementById('searchResults');
if(!q||q.length<1){el.classList.remove('show');return}
const results=[];
allV.forEach(v=>{if((v.title||'').toLowerCase().includes(q)||(v.author||'').toLowerCase().includes(q)||(v.discipline||'').toLowerCase().includes(q))results.push({t:t('videoTitle')||'Video',title:v.title,id:v.id,type:'video'})});
allK.forEach(k=>{if((k.title||'').toLowerCase().includes(q)||(k.description||'').toLowerCase().includes(q))results.push({t:t('cardLabel')||'Card',title:k.title,id:k.id,type:'knowledge'})});
if(results.length===0){el.innerHTML='<div style="padding:12px;color:#94A3B8;font-size:12px">'+t('noResult')+'</div>';el.classList.add('show');return}
el.innerHTML=results.slice(0,8).map(r=>'<div class="search-result-item" onclick="searchNavigate(\''+r.type+'\','+r.id+')"><span class="search-result-type">'+(r.t==='视频'?'Video':r.t==='知识'?'Card':r.t)+'</span>'+esc(r.title)+'</div>').join('');
el.classList.add('show');
},200);
}
function searchNavigate(type,id){
document.getElementById('globalSearch').value='';document.getElementById('searchResults').classList.remove('show');
if(type==='video'){navigateTo('discover');setTimeout(()=>openVideo(id,'','','','',''),500)}
else if(type==='knowledge'){navigateTo('discover');setTimeout(()=>openKnowledgeDetail(id),500)}
}
document.addEventListener('click',e=>{if(!e.target.closest('.sidebar-search'))document.getElementById('searchResults').classList.remove('show')});
function toggleAIPanel(){var p=document.getElementById('aiPanel');if(!p)return;var m=document.getElementById('toolModal');if(m)m.classList.remove('show');if(p.classList.contains('open')){p.classList.remove('open')}else{p.classList.add('open')}}
const _apiCache={},_CACHEABLE=['/api/videos','/api/knowledge','/api/story','/api/simulator','/api/math_personality'];
async function api(p,o={}){const u=API_BASE+p,c={headers:{'Content-Type':'application/json'},...o};if(c.body&&typeof c.body==='object')c.body=JSON.stringify(c.body);const isGet=!c.method;if(isGet&&_CACHEABLE.some(x=>p.indexOf(x)===0)&&_apiCache[u])return _apiCache[u];const r=await fetch(u,c);const j=await r.json();if(isGet&&_CACHEABLE.some(x=>p.indexOf(x)===0))_apiCache[u]=j;return j}
function navigateTo(p){
CP=p;closeSidebar();closeToolModal();document.getElementById('searchResults').classList.remove('show');
if(p!=='home'&&heroAnimId){cancelAnimationFrame(heroAnimId);heroAnimId=null}
document.querySelectorAll('.nav-item').forEach(b=>b.classList.toggle('active',b.dataset.page===p));
document.querySelectorAll('.page').forEach(pg=>pg.classList.remove('active'));
const t=document.getElementById('page-'+p);if(t)t.classList.add('active');
document.getElementById('mainContent').scrollTop=0;
if(p==='home')loadHome();else if(p==='discover')loadDiscover();else if(p==='toolbox')loadToolbox();
else if(p==='knowledge')loadKnowledgePage();else if(p==='story')loadStory();else if(p==='simulator')loadSimulator();else if(p==='favorites')loadFavorites();else if(p==='profile')loadProfile();else if(p==='cards')loadCardsPage();
}
let heroAnimId=null;
async function loadHome(){
chatCtx={};
setTimeout(drawHeroCanvas,100);
const pet=JSON.parse(localStorage.getItem('mathbrain_pet')||'null');
const pa=document.getElementById('homePetArea');
if(pet){pa.innerHTML='<div class="pet-card"><div class="pet-avatar">'+pet.icon+'</div><div class="pet-info"><div class="pet-name">'+esc(pet.name)+'</div><div class="pet-type">🎓 '+esc(pet.type)+' · Lv.'+pet.level+' · '+pet.exp+'/100 exp</div><div class="pet-exp-bar"><div class="pet-exp-fill" style="width:'+(pet.exp%100)+'%"></div></div><div class="pet-level">'+t('petTip')+'</div></div></div>';}
else{pa.innerHTML='';}
document.getElementById('homeFeatureCards').innerHTML=[
{i:'🧬',n:t('mbtiName'),d:t('mbtiDesc'),click:"window.open('/mathbti','_blank')"},
{i:'🧰',n:t('toolboxName'),d:t('toolboxDesc'),click:"navigateTo('toolbox')"},
{i:'⚔️',n:t('storyName'),d:t('storyDesc'),click:"navigateTo('story')"},
{i:'🎮',n:t('simName'),d:t('simDesc'),click:"navigateTo('simulator')"}
].map(c=>'<div class="ds-feature-card" onclick="'+c.click+'"><div class="ds-feature-card-icon">'+c.i+'</div><div class="ds-feature-card-name">'+c.n+'</div><div class="ds-feature-card-desc">'+c.d+'</div></div>').join('');
document.getElementById('homeExtraCards').innerHTML=[
{i:'📺',n:t('videoName'),d:t('videoDesc'),click:"navigateTo('discover')"},
{i:'📚',n:t('cardName'),d:t('cardDesc'),click:"navigateTo('knowledge')"},
{i:'💬',n:t('aiName'),d:t('aiDesc'),click:"toggleAIPanel()"}
].map(c=>'<div class="ds-feature-card" onclick="'+c.click+'"><div class="ds-feature-card-icon">'+c.i+'</div><div class="ds-feature-card-name">'+c.n+'</div><div class="ds-feature-card-desc">'+c.d+'</div></div>').join('');
const facts=['π的小数点后第762位开始连续出现六个9,被称为「费曼点」。','世界上最大的已知质数有超过4100万位数字。','哥德巴赫猜想:每个大于2的偶数都可以写成两个质数之和——至今未被证明。','四色定理:任何地图只需4种颜色就能让相邻区域颜色不同。1976年用计算机证明。','莫比乌斯带只有一面——拿一条纸带,扭转180°后粘起来试试。'];
document.getElementById('homeDailyFact').innerHTML=t('dailyFactPrefix')+facts[Math.floor(Math.random()*facts.length)];
rCG();
}
function drawHeroCanvas(){
const c=document.getElementById('heroCanvas');if(!c)return;
if(heroAnimId){cancelAnimationFrame(heroAnimId);heroAnimId=null}
const ctx=c.getContext('2d'),w=c.width,h=c.height;
let t=0;
function frame(){
ctx.clearRect(0,0,w,h);
ctx.strokeStyle='#D6D1CA';ctx.lineWidth=1;
for(let i=0;i<w;i+=50){ctx.beginPath();ctx.moveTo(i,0);ctx.lineTo(i,h);ctx.stroke()}
for(let i=0;i<h;i+=50){ctx.beginPath();ctx.moveTo(0,i);ctx.lineTo(w,i);ctx.stroke()}
ctx.strokeStyle='#808C95';ctx.lineWidth=2.5;ctx.beginPath();
for(let px=0;px<w;px++){const x=(px-w/2)/60;const y=Math.sin(x+t)*Math.cos(x*0.7+t*0.5)*50+h/2;if(px===0)ctx.moveTo(px,y);else ctx.lineTo(px,y)}ctx.stroke();
ctx.strokeStyle='#9AA5AE';ctx.lineWidth=1.5;ctx.beginPath();
for(let px=0;px<w;px++){const x=(px-w/2)/60;const y=Math.cos(x*1.3-t*0.7)*45+h/2;if(px===0)ctx.moveTo(px,y);else ctx.lineTo(px,y)}ctx.stroke();
if(Math.sin(t*0.3)>0.7){
ctx.save();ctx.globalAlpha=Math.min(1,(Math.sin(t*0.3)-0.7)/0.3*0.5);
ctx.strokeStyle='#A8907E';ctx.lineWidth=1;
const gx=w-60,gy=h-30;
ctx.beginPath();
ctx.ellipse(gx,gy-28,12,9,0,0,Math.PI*2);
ctx.ellipse(gx,gy,16,13,0,0,Math.PI*2);
ctx.stroke();
ctx.restore();
}
t+=0.02;heroAnimId=requestAnimationFrame(frame);
}
frame();
}
async function loadDiscover(){
chatCtx={};
if(favP){await favP;favP=null}
if(allV.length){rDF();rVCNP('discoverVideos',allV);return}
renderSkeletons('discoverVideos',6);
try{const r=await api('/api/videos?module=数人');if(r.code===0){allV=r.data||[];vD=r.disciplines||[];rDF();rVCNP('discoverVideos',allV)}}catch(e){document.getElementById('discoverVideos').innerHTML='<p style="color:#8A8580;text-align:center;padding:40px;grid-column:1/-1">'+t('loadFailVideo')+'</p>'}
}
const VC_PAGE=12;
let vcList=[],vcCount=0,vcObserver=null;
const UP_COLORS=['#5B6AF0','#818CF8','#F59E0B','#10B981','#EF4444','#8B5CF6','#EC4899','#14B8A6'];
function hashStr(s){let h=0;for(let i=0;i<s.length;i++){h=((h<<5)-h)+s.charCodeAt(i);h|=0}return Math.abs(h)}
function fmtNum(n){return n>=10000?(n/10000).toFixed(1)+'万':''+n}
function fmtDur(m){m=parseInt(m)||0;return(m<10?'0'+m:''+m)+':00'}
function rVCNP(cid,vids){
const c=document.getElementById(cid);
vcList=vids||[];vcCount=0;c.innerHTML='';
if(!vcList.length){c.innerHTML='<p style="color:#94A3B8;text-align:center;padding:40px;grid-column:1/-1">'+t('noVideo')+'</p>';return}
appendVC(cid);
}
function appendVC(cid){
const c=document.getElementById(cid);
const batch=vcList.slice(vcCount,vcCount+VC_PAGE);
if(!batch.length)return;
vcCount+=batch.length;
const old=document.getElementById('npLoadHint');if(old)old.remove();
c.insertAdjacentHTML('beforeend',batch.map(buildVC).join(''));
if(vcCount<vcList.length){
c.insertAdjacentHTML('beforeend','<div class="np-load-hint" id="npLoadHint">'+t('loadMore')+'</div>');
if(!vcObserver){vcObserver=new IntersectionObserver(es=>{es.forEach(en=>{if(en.isIntersecting)appendVC('discoverVideos')})},{root:document.getElementById('mainContent'),rootMargin:'240px'})}
vcObserver.observe(document.getElementById('npLoadHint'));
}else if(vcList.length>VC_PAGE){
c.insertAdjacentHTML('beforeend','<div class="np-load-hint">'+t('loadEnd')+vcList.length+t('loadEndSuf')+'</div>');
}
}
function buildVC(v){
const dur=v.duration_min||5,disc=v.discipline||'',author=v.author||'';
const icon=v.discipline_icon&&v.discipline_icon!=='·'?v.discipline_icon:'📺';
const h=hashStr(disc||v.title);
const coverBg='linear-gradient(135deg,hsl('+(h%360)+',45%,90%),hsl('+((h+30)%360)+',45%,84%))';
const views=hashStr(v.title)%50000+300,dms=hashStr(v.title+'#dm')%900;
const upColor=UP_COLORS[hashStr(author)%UP_COLORS.length];
const initial=(author||'?').replace(/学院|·|同学|系/g,'').slice(0,1)||'?';
const faved=favC['video_'+v.id];
const click='onclick="openVideo('+v.id+',\''+esc(v.title)+'\',\''+esc(v.description||'')+'\',\''+esc(v.video_url||'')+'\',\''+esc(author)+'\',\''+esc(disc)+'\')"';
const cover=v.cover_url
?'<div class="np-cover-inner"><img class="np-cover-img" src="'+esc(v.cover_url)+'" alt="" loading="lazy" onerror="this.parentNode.style.background=\''+coverBg+'\'"></div>'
:'<div class="np-cover-inner" style="background:'+coverBg+'"><span class="np-cover-icon">'+icon+'</span></div>';
return '<div class="np-card" '+click+'>'+
'<div class="np-cover">'+cover+
'<span class="np-cover-duration">'+fmtDur(dur)+'</span>'+
'<span class="np-cover-play">▶</span>'+
'<button class="np-cover-fav'+(faved?' faved':'')+'" onclick="event.stopPropagation();tF(\'video\','+v.id+',\''+esc(v.title)+'\',\''+esc(icon)+'\')">'+(faved?'❤️':'🤍')+'</button></div>'+
'<div class="np-body"><div class="np-card-title">'+esc(v.title)+'</div>'+
'<div class="np-stats"><span class="np-stat">👁 '+fmtNum(views)+'</span><span class="np-stat">💬 '+fmtNum(dms)+'</span></div>'+
'<div class="np-uploader"><span class="np-up-avatar" style="background:'+upColor+'">'+esc(initial)+'</span><span class="np-up-name">'+esc(author)+'</span></div>'+
'</div></div>';
}
async function loadKnowledgePage(){
chatCtx={type:'knowledge'};
if(allK.length){rKCFilters();rKCCards();rKP();return}
try{const r=await api('/api/knowledge');if(r.code===0){allK=r.data||[];kT=r.topics||[];rKCFilters();rKCCards()}}catch(e){document.getElementById('knowledgeGrid').innerHTML='<p style="color:#94A3B8;text-align:center;padding:40px;grid-column:1/-1">卡片加载失败</p>'}}
function rKCFilters(){let h='<button class="filter-btn'+(cT===''?' active':'')+'" onclick="fKC(\'\')">'+t('filterAll')+'</button>';kT.forEach(t=>{h+='<button class="filter-btn'+(cT===t?' active':'')+'" onclick="fKC(\''+esc(t)+'\')">'+esc(t)+'</button>'});document.getElementById('knowledgeFilters').innerHTML=h}
function fKC(t){cT=t;rKCFilters();rKCCards()}
function rKP(){
const bar=document.getElementById('knowledgeDrawProgress');
if(!bar)return;
const readThisSession=(cardState.knowledgeRead||0)%3;
const needMore=3-readThisSession;
bar.style.display='flex';
if(needMore<=0){
document.getElementById('drawProgressText').textContent='🎉 可以抽卡了!点击去抽卡';
document.getElementById('drawProgressFill').style.width='100%';
bar.classList.add('draw-progress-ready');
}else{
document.getElementById('drawProgressText').textContent='再读 '+needMore+' 张知识卡片即可抽卡';
document.getElementById('drawProgressFill').style.width=(readThisSession/3*100)+'%';
bar.classList.remove('draw-progress-ready');
}
}
function rKCCards(){const cards=cT?allK.filter(c=>c.topic===cT):allK;const g=document.getElementById('knowledgeGrid');if(!cards||cards.length===0){g.innerHTML='<p style="color:#94A3B8;text-align:center;padding:40px;grid-column:1/-1">'+t('noCard')+'</p>';return}g.innerHTML=cards.map(c=>'<div class="knowledge-card" onclick="openKnowledgeDetail('+c.id+')"><div class="knowledge-card-visual" style="background:linear-gradient(135deg,'+c.color+','+adj(c.color,30)+')"><span style="font-size:42px;z-index:1">'+c.icon+'</span><div class="knowledge-card-question">'+esc(c.question)+'</div></div><div class="knowledge-card-body"><div class="knowledge-card-title">'+esc(c.title)+'</div><div class="knowledge-card-desc">'+esc(c.description)+'</div><span class="knowledge-card-time">'+esc(c.read_time)+'</span></div></div>').join('');setTimeout(rKP,100)}
function rDF(){let h='<button class="filter-btn'+(cD===''?' active':'')+'" onclick="fV(\'\')">'+t('filterAll')+'</button>';vD.forEach(d=>{h+='<button class="filter-btn'+(cD===d.discipline?' active':'')+'" onclick="fV(\''+esc(d.discipline)+'\')">'+(d.discipline_icon||'')+' '+esc(d.discipline)+'</button>'});document.getElementById('discoverFilters').innerHTML=h}
function fV(d){cD=d;rDF();rVCNP('discoverVideos',d?allV.filter(v=>v.discipline===d):allV)}
function openVideo(id,title,desc,url,author,disc){chatCtx={type:'video',title:title};rH('video',id,title);let h='<h2 style="font-size:18px;font-weight:700;margin-bottom:12px">'+esc(title)+'</h2>';h+=url?'<div style="aspect-ratio:16/9;background:#111;border-radius:12px;margin-bottom:12px;display:flex;align-items:center;justify-content:center"><iframe src="'+esc(url)+'" style="width:100%;height:100%;border-radius:12px" frameborder="0" allowfullscreen></iframe></div>':'<div style="aspect-ratio:16/9;background:#F1F5F9;border-radius:12px;margin-bottom:12px;display:flex;align-items:center;justify-content:center;font-size:48px;color:#CBD5E1">▶</div>';h+='<p style="font-size:14px;color:#64748B;line-height:1.7;margin-bottom:8px">'+esc(desc||'No description')+'</p><div style="display:flex;gap:8px;font-size:12px;color:#94A3B8"><span>'+esc(author)+'</span><span>'+esc(disc)+'</span></div><div style="margin-top:12px"><button onclick="closeToolModal()" style="padding:8px 20px;border-radius:10px;border:1.5px solid #E2E8F0;background:#fff;cursor:pointer;font-size:13px">'+t('close')+'</button></div>';document.getElementById('toolModalContent').innerHTML=h;document.getElementById('toolModal').classList.add('show')}
async function openKnowledgeDetail(id){chatCtx={type:'knowledge',title:''};
cardState.knowledgeRead=(cardState.knowledgeRead||0)+1;
if(cardState.knowledgeRead>=3&&cardState.knowledgeRead%3===0){
cardState.draws+=1;
saveCardState();
}else{
saveCardState();
}
rKP();
try{const r=await api('/api/knowledge/'+id);if(r.code===0){const c=r.data;chatCtx.title=c.title;document.getElementById('toolModalContent').innerHTML='<div style="width:100%;height:80px;border-radius:12px;background:linear-gradient(135deg,'+c.color+','+adj(c.color,30)+');display:flex;align-items:center;justify-content:center;font-size:40px;margin-bottom:16px">'+c.icon+'</div><h2 style="font-size:20px;font-weight:700;margin-bottom:4px">'+esc(c.title)+'</h2><p style="font-size:13px;color:#5B6AF0;font-weight:600;margin-bottom:12px">'+esc(c.question)+'</p><p style="font-size:14px;color:#475569;line-height:1.8">'+esc(c.detail||c.description)+'</p><div style="margin-top:16px;display:flex;justify-content:space-between;align-items:center"><span style="font-size:12px;color:#94A3B8">'+esc(c.read_time)+' · '+esc(c.topic||'')+'</span><button onclick="qA(\''+esc(c.title)+t('aiAskSuffix')+'\')" style="padding:6px 14px;border-radius:20px;border:none;background:#5B6AF0;color:#fff;font-size:12px;cursor:pointer">🤖 '+t('aiAskBtn')+'</button></div>'}}catch(e){}document.getElementById('toolModal').classList.add('show')}
function closeToolModal(){var m=document.getElementById('toolModal');if(m)m.classList.remove('show')}
function loadToolbox(){chatCtx={}}
let toolState={};
function openTool(name){
let h='';
if(name==='mathrace'){toolState={score:0,total:0,time:60,timer:null,correct:0};h=rMR()}
else if(name==='calculator')h=rCalc();
else if(name==='symbols')h=rSym();
else if(name==='grapher')h=rGraph();
document.getElementById('toolModalContent').innerHTML=h;
document.getElementById('toolModal').classList.add('show');
if(name==='mathrace')startMRLoop();
if(name==='grapher')setTimeout(plotGraph,200);
}
function rMR(){return'<div class="math-race"><div class="timer-bar"><div class="timer-fill" id="mrTimer" style="width:100%"></div></div><div class="stats"><span>✅ <b id="mrScore">0</b></span><span>⏱ <b id="mrTime">60s</b></span><span>📝 <b id="mrTotal">0</b></span></div><div class="question" id="mrQuestion">准备好了吗?</div><div class="answer-area"><input type="number" id="mrAnswer" onkeydown="if(event.key===\'Enter\')checkMR()" placeholder="输入答案" autofocus></div><div class="result" id="mrResult"></div></div>'}
function startMRLoop(){toolState={score:0,total:0,time:60,timer:null,correct:0};document.getElementById('mrScore').textContent='0';document.getElementById('mrTime').textContent='60s';document.getElementById('mrTotal').textContent='0';document.getElementById('mrResult').textContent='';document.getElementById('mrAnswer').value='';document.getElementById('mrAnswer').disabled=false;document.getElementById('mrAnswer').focus();genMR();toolState.timer=setInterval(()=>{toolState.time--;document.getElementById('mrTime').textContent=toolState.time+'s';document.getElementById('mrTimer').style.width=(toolState.time/60*100)+'%';if(toolState.time<=0){clearInterval(toolState.timer);document.getElementById('mrAnswer').disabled=true;document.getElementById('mrResult').innerHTML='⏰ 时间到!得分:<b>'+toolState.score+'</b> 正确:'+toolState.correct+'/'+toolState.total;addPetExp(toolState.score)}},1000);genMR()}
function genMR(){const ops=['+','-','×'];const op=ops[Math.floor(Math.random()*3)];let a,b;if(op==='+'){a=Math.floor(Math.random()*90)+10;b=Math.floor(Math.random()*90)+10}else if(op==='-'){a=Math.floor(Math.random()*90)+10;b=Math.floor(Math.random()*a)}else{a=Math.floor(Math.random()*12)+2;b=Math.floor(Math.random()*12)+2}toolState.answer=op==='+'?a+b:op==='-'?a-b:a*b;toolState.total++;document.getElementById('mrTotal').textContent=toolState.total;document.getElementById('mrQuestion').textContent=a+' '+op+' '+b+' = ?';document.getElementById('mrAnswer').value='';document.getElementById('mrAnswer').focus()}
function checkMR(){const ans=parseInt(document.getElementById('mrAnswer').value);if(isNaN(ans))return;if(ans===toolState.answer){toolState.score+=10;toolState.correct++;document.getElementById('mrResult').innerHTML='<span style="color:#8AA68A">✅ '+t('mrRight')+'</span>'}else{document.getElementById('mrResult').innerHTML='<span style="color:#C49595">❌ '+toolState.answer+'</span>'}document.getElementById('mrScore').textContent=toolState.score;if(toolState.time>0)setTimeout(genMR,600)}
function rCalc(){return'<div style="padding:20px"><div style="display:flex;gap:8px;margin-bottom:16px"><input id="calcInput" style="flex:1;padding:12px;border:1.5px solid #E2E8F0;border-radius:12px;font-size:18px;text-align:right;outline:none" placeholder="输入表达式 如 2+3*4" onkeydown="if(event.key===\'Enter\')doCalc()"><button onclick="doCalc()" style="padding:12px 24px;border-radius:12px;border:none;background:#5B6AF0;color:#fff;font-size:16px;cursor:pointer">=</button></div><div style="text-align:right;font-size:24px;font-weight:800;min-height:36px" id="calcResult"></div><div style="display:grid;grid-template-columns:repeat(4,1fr);gap:8px;margin-top:16px">'+['(',')','C','÷','7','8','9','×','4','5','6','-','1','2','3','+','0','.','sin(','√('].map(k=>'<button onclick="calcKey(\''+k+'\')" style="padding:12px;border:1.5px solid #E2E8F0;border-radius:10px;background:#FFF;cursor:pointer;font-size:14px;font-weight:600;transition:all .15s">'+k+'</button>').join('')+'</div></div>'}
function calcKey(k){const i=document.getElementById('calcInput');if(k==='C')i.value='';else if(k==='÷')i.value+='/';else if(k==='×')i.value+='*';else if(k==='sin(')i.value+='Math.sin(';else if(k==='√(')i.value+='Math.sqrt(';else i.value+=k;i.focus()}
function doCalc(){try{const v=eval(document.getElementById('calcInput').value);document.getElementById('calcResult').textContent=Number.isInteger(v)?v:v.toFixed(6)}catch(e){document.getElementById('calcResult').textContent='表达式有误'}}
function rSym(){
const syms=[['π','Pi · 圆周率','3.14159... · \\pi'],['∞','无穷大','没有边界 · \\infty'],['Σ','求和符号','累加 · \\sum'],['∫','积分符号','面积/累积 · \\int'],['√','平方根','x²=a → x=√a · \\sqrt{}'],['Δ','变化量/判别式','差值△y/△x · \\Delta'],['∂','偏导数','多元函数求导 · \\partial'],['lim','极限','趋近于 · \\lim_{x \\to 0}'],['e','自然常数','≈2.71828 · e'],['i','虚数单位','i² = -1 · i'],['∏','连乘符号','累乘 · \\prod'],['∈','属于','元素∈集合 · \\in']];
return'<div class="symbol-grid">'+syms.map(s=>'<button class="symbol-btn" onclick="sS(\''+s[0]+'\',\''+s[1]+'\',\''+s[2]+'\')">'+s[0]+'<div style="font-size:10px;color:#94A3B8">'+s[1]+'</div></button>').join('')+'</div><div class="symbol-detail" id="symDetail">👆 点击符号查看详情</div>'
}
function sS(sym,name,latex){document.getElementById('symDetail').innerHTML='<b style="font-size:28px">'+sym+'</b><div>'+name+'</div><div style="color:#5B6AF0;font-family:monospace">LaTeX: '+latex+'</div>'}
function rGraph(){return'<div class="graph-container"><canvas id="graphCanvas" width="500" height="300"></canvas></div><div class="graph-inputs"><input id="graphFunc" value="Math.sin(x)" placeholder="如 Math.sin(x) 或 x*x"><button onclick="plotGraph()">绘制</button></div>'}
function plotGraph(){const c=document.getElementById('graphCanvas');if(!c)return;const ctx=c.getContext('2d');ctx.clearRect(0,0,c.width,c.height);ctx.strokeStyle='#D6D1CA';ctx.lineWidth=1;for(let i=0;i<=c.width;i+=50){ctx.beginPath();ctx.moveTo(i,0);ctx.lineTo(i,c.height);ctx.stroke()}for(let i=0;i<=c.height;i+=50){ctx.beginPath();ctx.moveTo(0,i);ctx.lineTo(c.width,i);ctx.stroke()}ctx.strokeStyle='#808C95';ctx.lineWidth=2;ctx.beginPath();const fn=new Function('x','return '+document.getElementById('graphFunc').value);const sx=c.width/2,sy=c.height/2,scale=30;for(let px=0;px<c.width;px++){const x=(px-sx)/scale;try{const y=fn(x);const py=sy-y*scale;if(py>-100&&py<c.height+100){if(px===0)ctx.moveTo(px,py);else ctx.lineTo(px,py)}}catch(e){}}ctx.stroke()}
async function loadStory(){chatCtx={};if(tt)clearTimeout(tt);try{const r=await api('/api/story');if(r.code===0){sD=r.data;const saved=localStorage.getItem('mb_story_save');if(saved){try{sVN=JSON.parse(saved)}catch(e){}}const sn=sD.start_node||'ch1_start';const ni=(sVN.currentNode&&sD.nodes&&sD.nodes[sVN.currentNode])?sVN.currentNode:sn;sVN.currentNode=ni;rSN(ni)}}catch(e){document.getElementById('storyScene').innerHTML='<p style="color:#EF4444">故事加载失败</p>'}}
function rSN(ni){if(!sD||!sD.nodes||!sD.nodes[ni]){document.getElementById('storyScene').innerHTML='<p style="color:#EF4444">节点未找到</p>';return}const n=sD.nodes[ni];sVN.currentNode=ni;localStorage.setItem('mb_story_save',JSON.stringify(sVN));const ch=dC(ni),cN=['','初入江湖','西部教习','数理论道','脑学初创','报国之路','暗流涌动','陋室修炼','真相之门'];document.getElementById('storyStats').innerHTML='<span class="story-stat" style="background:#808C95">📐 '+(sVN.stats.shuli||10)+'</span><span class="story-stat" style="background:#C4A467">⚔️ '+(sVN.stats.xiayi||10)+'</span><span class="story-stat" style="background:#8AA68A">🧘 '+(sVN.stats.xinzhi||10)+'</span><span class="story-stat" style="background:#657078">'+ch+' · '+(cN[ch]||'')+'</span><button onclick="resetStory()" style="margin-left:auto;padding:6px 14px;border-radius:8px;border:1px solid #D6D1CA;background:#FDFCFA;cursor:pointer;font-size:12px;color:#8A8580">🔄 '+t('simReset')+'</button>';document.getElementById('storyScene').innerHTML=(n.character?'<div class="story-character">'+esc(n.character)+'</div>':'')+'<div class="story-text" id="storyText"></div>';document.getElementById('storyChoices').innerHTML='';if(tt)clearTimeout(tt);const te=document.getElementById('storyText'),tx=n.scene||'';let i=0;te.textContent='';function ty(){if(i<tx.length){te.textContent+=tx.charAt(i);i++;tt=setTimeout(ty,18)}else{rSCs(n.choices||[])}}ty()}
function rSCs(choices){document.getElementById('storyChoices').innerHTML=choices.map((c,i)=>{let h='';if(c.effects){const p=[];if(c.effects.shuli)p.push('数理根基'+(c.effects.shuli>0?'+':'')+c.effects.shuli);if(c.effects.xiayi)p.push('侠义声望'+(c.effects.xiayi>0?'+':'')+c.effects.xiayi);if(c.effects.xinzhi)p.push('心志韧性'+(c.effects.xinzhi>0?'+':'')+c.effects.xinzhi);if(p.length)h='<span style="font-size:11px;color:#8B5CF6;font-weight:500">['+p.join(' | ')+']</span>'}return'<button class="story-choice" onclick="sC(\''+c.next+'\','+JSON.stringify(c.effects||{})+')">'+esc(c.text)+h+'</button>'}).join('')}
function sC(next,ef){aSE(ef);rSN(next)}
function aSE(ef){if(!ef)return;if(ef.shuli)sVN.stats.shuli=(sVN.stats.shuli||10)+ef.shuli;if(ef.xiayi)sVN.stats.xiayi=(sVN.stats.xiayi||10)+ef.xiayi;if(ef.xinzhi)sVN.stats.xinzhi=(sVN.stats.xinzhi||10)+ef.xinzhi;if(ef.favorability){if(!sVN.fav)sVN.fav={};for(const k in ef.favorability)sVN.fav[k]=(sVN.fav[k]||0)+ef.favorability[k]}}
function resetStory(){if(tt)clearTimeout(tt);sVN={currentNode:null,stats:{shuli:10,xiayi:10,xinzhi:10},fav:{}};localStorage.removeItem('mb_story_save');loadStory()}
function dC(ni){if(!ni)return 1;const m=ni.match(/^ch(\d+)/);return m?parseInt(m[1]):1}
async function loadSimulator(){chatCtx={type:'simulator'};try{const r=await api('/api/simulator');if(r.code===0){simD=r.data;try{const sr=await api('/api/simulator/save?user_id='+USER_ID);if(sr.code===0&&sr.data)simS=sr.data}catch(e){}const sn=simD.start_node||'y1_welcome';if(!simS.currentNode||!simD.nodes||!simD.nodes[simS.currentNode])simS.currentNode=sn;rSiN(simS.currentNode)}}catch(e){document.getElementById('simScene').innerHTML='<p style="color:#EF4444">模拟器加载失败</p>'}}
function rSiN(ni){if(!simD||!simD.nodes||!simD.nodes[ni]){document.getElementById('simScene').innerHTML='<p style="color:#EF4444">节点未找到</p>';return}const n=simD.nodes[ni];simS.currentNode=ni;api('/api/simulator/save',{method:'POST',body:{user_id:USER_ID,state:simS}}).catch(()=>{});document.getElementById('simStats').innerHTML='<div class="sim-stat"><div class="sim-stat-val" style="color:#808C95">'+(simS.stats.gpa||3).toFixed(1)+'</div><div class="sim-stat-label">GPA</div></div><div class="sim-stat"><div class="sim-stat-val" style="color:#C4A467">'+(simS.stats.interest||0)+'</div><div class="sim-stat-label">'+t('interestLabel')+'</div></div><div class="sim-stat"><div class="sim-stat-val" style="color:#8AA68A">'+(simS.stats.skill||0)+'</div><div class="sim-stat-label">'+t('skillLabel')+'</div></div><button onclick="resetSim()" style="margin-left:auto;padding:6px 14px;border-radius:8px;border:1px solid #D6D1CA;background:#FDFCFA;cursor:pointer;font-size:12px;color:#8A8580">🔄 '+t('simReset')+'</button>';document.getElementById('simScene').innerHTML=(n.stage?'<div style="font-size:12px;color:#5B6AF0;font-weight:600;margin-bottom:8px">📅 '+esc(n.stage)+'</div>':'')+(n.character?'<div class="story-character">'+esc(n.character)+'</div>':'')+'<div class="story-text">'+esc(n.scene||'')+'</div>';document.getElementById('simChoices').innerHTML=(n.choices||[]).map(c=>'<button class="story-choice" onclick="siC(\''+c.next+'\','+JSON.stringify(c.effects||{})+')">'+esc(c.text)+(c.hint?'<span style="font-size:11px;color:#94A3B8;font-weight:400;display:block;margin-top:4px">'+esc(c.hint)+'</span>':'')+'</button>').join('')}
function siC(next,ef){if(ef){if(ef.gpa)simS.stats.gpa=Math.max(0,Math.min(4,(simS.stats.gpa||3)+ef.gpa*.1));if(ef.interest)simS.stats.interest=(simS.stats.interest||0)+ef.interest;if(ef.skill)simS.stats.skill=(simS.stats.skill||0)+ef.skill}rSiN(next);addPetExp(5)}
function resetSim(){simS={currentNode:null,stats:{gpa:3,interest:0,skill:0}};loadSimulator()}
async function loadFavorites(){chatCtx={};try{await rFC()}catch(e){}try{const r=await api('/api/favorites?user_id='+USER_ID);const g=document.getElementById('favGrid');if(r.code===0){const f=r.data||[];if(f.length===0){g.innerHTML='<p style="color:#94A3B8;text-align:center;padding:60px;grid-column:1/-1">暂无收藏<br><span style="font-size:12px;margin-top:8px;display:block">在发现页点击 🤍 即可收藏</span></p>';return}g.innerHTML=f.map(fv=>'<div class="fav-card"><button class="fav-card-remove" onclick="rF(\''+fv.target_type+'\','+fv.target_id+')">✕</button><div style="font-size:11px;color:#94A3B8;margin-bottom:4px">'+(fv.target_type==='video'?'📺 视频':'📚 知识卡片')+'</div><div style="font-size:14px;font-weight:600">'+esc(fv.title)+'</div></div>').join('')}}catch(e){document.getElementById('favGrid').innerHTML='<p style="color:#94A3B8;text-align:center;padding:40px">加载失败</p>'}}
async function rFC(){try{const r=await api('/api/favorites?user_id='+USER_ID);if(r.code===0){favC={};(r.data||[]).forEach(f=>{favC[f.target_type+'_'+f.target_id]=true})}}catch(e){}}
async function tF(type,id,title,cover){if(favC[type+'_'+id]){await api('/api/favorites',{method:'DELETE',body:{user_id:USER_ID,target_type:type,target_id:id}});favC[type+'_'+id]=false;showToast('🗑 '+t('close')+' ✔')}else{await api('/api/favorites',{method:'POST',body:{user_id:USER_ID,target_type:type,target_id:id,title:title,cover:cover}});favC[type+'_'+id]=true;showToast('❤ '+t('saveOK'))}if(CP==='discover')fV(cD);else if(CP==='favorites')loadFavorites()}
async function rF(type,id){await api('/api/favorites',{method:'DELETE',body:{user_id:USER_ID,target_type:type,target_id:id}});loadFavorites()}
async function rH(type,id,title){try{await api('/api/history',{method:'POST',body:{user_id:USER_ID,target_type:type,target_id:id,title:title}})}catch(e){}}
async function loadProfile(){chatCtx={};try{const[pr,sr]=await Promise.all([api('/api/user/profile?user_id='+USER_ID),api('/api/user/stats?user_id='+USER_ID)]);const p=pr.code===0?pr.data:{nickname:'游客用户',avatar_emoji:'葫',avatar_color:'#5B6AF0'};const s=sr.code===0?sr.data:{watch_count:0,favorite_count:0};document.getElementById('profileContent').innerHTML='<div class="profile-card"><div class="profile-avatar-lg" style="background:rgba(255,255,255,.2)">'+esc(p.avatar_emoji||'葫')+'</div><div><div class="profile-name-lg">'+esc(p.nickname||'游客用户')+'</div><div class="profile-id-lg">ID: '+USER_ID+'</div></div></div><div class="profile-stats-grid"><div class="profile-stat-card"><div class="num">'+s.watch_count+'</div><div class="label">观看次数</div></div><div class="profile-stat-card"><div class="num">'+s.favorite_count+'</div><div class="label">收藏数量</div></div></div><div style="background:#fff;border-radius:16px;padding:16px 20px;box-shadow:0 1px 3px rgba(0,0,0,.04)"><div style="font-weight:600;margin-bottom:12px">编辑资料</div><div style="display:flex;gap:12px;align-items:center;margin-bottom:12px"><span style="font-size:13px;color:#64748B">头像:</span><input id="editAvatar" style="padding:8px 12px;border:1.5px solid #E2E8F0;border-radius:10px;font-size:14px;width:80px" value="'+esc(p.avatar_emoji||'葫')+'"></div><div style="display:flex;gap:12px;align-items:center;margin-bottom:16px"><span style="font-size:13px;color:#64748B">昵称:</span><input id="editNickname" style="flex:1;padding:8px 12px;border:1.5px solid #E2E8F0;border-radius:10px;font-size:14px" value="'+esc(p.nickname||'')+'"></div><button onclick="sP()" style="padding:10px 28px;border-radius:12px;border:none;background:#5B6AF0;color:#fff;font-size:14px;font-weight:600;cursor:pointer">保存</button></div>'}catch(e){document.getElementById('profileContent').innerHTML='<p style="color:#94A3B8">加载失败</p>'}}
async function sP(){const n=document.getElementById('editNickname').value.trim();const a=document.getElementById('editAvatar').value.trim();try{await api('/api/user/profile',{method:'POST',body:{user_id:USER_ID,nickname:n,avatar_emoji:a}});document.getElementById('sidebarName').textContent=n||'游客用户';document.getElementById('sidebarAvatar').textContent=a||'葫';alert('保存成功');loadProfile()}catch(e){alert('保存失败')}}
async function loadCardsPage(){
chatCtx={};
document.getElementById('cardsProgress').textContent=cardState.owned.length+'/16';
const drawBtn=document.getElementById('cardsDrawBtn');
const drawCount=document.getElementById('cardsDrawCount');
drawCount.textContent=cardState.draws;
drawBtn.style.display=cardState.draws>0?'inline-flex':'none';
const grid=document.getElementById('cardsCollectionGrid');
await loadCardData();
const allCodes=getAllCardCodes();
if(!allCodes.length){grid.innerHTML='<p style="color:#8A8580;text-align:center;padding:40px;grid-column:1/-1">卡片数据加载中…</p>';return}
let h='';
allCodes.forEach(code=>{
const info=getCardInfo(code);
const clan=info?getClanInfo(info.clan):null;
const clanColor=clan?clan.color:'#A8907E';
const owned=cardState.owned.indexOf(code)!==-1;
h+='<div class="cc-card'+(owned?' owned':' locked')+'" style="--clan-color:'+clanColor+'"'+(owned?' onclick="openCardDetail(\''+code+'\')"':'')+'>'+
(owned
?'<div class="cc-card-icon" style="background:'+(info?info.icon_bg:'#F0F0F0')+'">'+(info?info.icon:'?')+'</div><div class="cc-card-name">'+(info?info.name:'???')+'</div><div class="cc-card-type">'+code+'</div><div class="cc-card-clan" style="background:'+clanColor+'15;color:'+clanColor+'">'+(clan?clan.emoji+' '+clan.name:'')+'</div>'
:'<div class="cc-card-icon locked-icon">?</div><div class="cc-card-name">???</div><div class="cc-card-type">未获得</div><div class="cc-card-clan" style="color:#ABA6A1">'+(clan?clan.emoji+' '+clan.name:'未知')+'</div>')+
'</div>';
});
grid.innerHTML=h;
}
function addPetExp(amt){
let pet=JSON.parse(localStorage.getItem('mathbrain_pet')||'null');
if(!pet)return;
pet.exp=(pet.exp||0)+amt;
let lvlU=false;
while(pet.exp>=100){pet.exp-=100;pet.level=(pet.level||1)+1;lvlU=true}
localStorage.setItem('mathbrain_pet',JSON.stringify(pet));
const bar=document.querySelector('.pet-exp-fill');
if(bar){const pct=pet.exp%100;bar.style.transition='none';bar.style.width='0%';requestAnimationFrame(()=>{requestAnimationFrame(()=>{bar.style.transition='width .4s cubic-bezier(.34,1.56,.64,1)';bar.style.width=pct+'%'})})}
if(lvlU){showToast('🎉 '+t('petLevelUp').replace('{n}',pet.level));burstConfetti(document.querySelector('.pet-card'))}
}
async function sendChat(){const i=document.getElementById('chatInput');const m=i.value.trim();if(!m)return;aCM('user',m);i.value='';const tid=aCT();try{const r=await api('/api/chat',{method:'POST',body:{message:m,context:chatCtx}});rCT(tid);aCM('assistant',r.code===0?r.reply:'抱歉,AI暂时无法回复')}catch(e){rCT(tid);aCM('assistant','网络异常,请稍后重试')}}
function qA(q){document.getElementById('chatInput').value=q;closeToolModal();toggleAIPanel();setTimeout(()=>sendChat(),300)}
function aCM(role,text){const c=document.getElementById('aiMessages');const d=document.createElement('div');d.className='chat-msg '+role;d.innerHTML='<div class="chat-bubble">'+esc(text)+'</div>';c.appendChild(d);c.scrollTop=c.scrollHeight}
function aCT(){const c=document.getElementById('aiMessages');const d=document.createElement('div');d.className='chat-msg assistant';d.innerHTML='<div class="chat-bubble"><span style="display:flex;gap:4px"><span style="width:6px;height:6px;border-radius:50%;background:#CBD5E1;animation:bounce 1s infinite"></span><span style="width:6px;height:6px;border-radius:50%;background:#CBD5E1;animation:bounce 1s infinite .2s"></span><span style="width:6px;height:6px;border-radius:50%;background:#CBD5E1;animation:bounce 1s infinite .4s"></span></span></div>';c.appendChild(d);c.scrollTop=c.scrollHeight;return d}
function rCT(el){if(el&&el.parentNode)el.remove()}
function esc(t){if(!t)return'';esc._d=esc._d||document.createElement('div');esc._d.textContent=t;return esc._d.innerHTML}
function adj(hex,amt){const n=parseInt(hex.replace('#',''),16);const r=Math.min(255,(n>>16)+amt);const g=Math.min(255,((n>>8)&0xFF)+amt);const b=Math.min(255,(n&0xFF)+amt);return '#'+((r<<16)|(g<<8)|b).toString(16).padStart(6,'0')}
function toggleHub(){document.getElementById('floatHub').classList.toggle('open')}
function closeHub(){document.getElementById('floatHub').classList.remove('open')}
document.addEventListener('click',e=>{if(!e.target.closest('.float-hub'))document.getElementById('floatHub').classList.remove('open')});
function initCardState(){
const raw=localStorage.getItem('hulu_card_state');
if(raw){try{cardState=JSON.parse(raw)}catch(e){}}
if(!cardState.owned)cardState.owned=[];
if(!cardState.draws&&cardState.draws!==0)cardState.draws=0;
if(!cardState.lastDaily)cardState.lastDaily='';
if(!cardState.knowledgeRead&&cardState.knowledgeRead!==0)cardState.knowledgeRead=0;
if(!cardState.drawHistory)cardState.drawHistory=[];
const today=new Date().toISOString().slice(0,10);
if(cardState.lastDaily!==today){
cardState.draws+=1;
cardState.lastDaily=today;
saveCardState();
}
}
function saveCardState(){localStorage.setItem('hulu_card_state',JSON.stringify(cardState));rCG();}
function ownedCount(){return cardState.owned.length}
function grantCard(code,method){
if(!code)return;
if(cardState.owned.indexOf(code)===-1){
cardState.owned.push(code);
cardState.drawHistory.push({code:code,date:new Date().toISOString().slice(0,10),method:method});
saveCardState();
return true;
}
return false;
}
async function loadCardData(){
if(cardData)return cardData;
try{
const r=await api('/api/math_personality');
if(r.code===0&&r.data&&r.data.results){cardData=r.data;cardCache=r.data;return cardData}
}catch(e){}
return null;
}
function getCardInfo(code){
if(!cardCache||!cardCache.results)return null;
return cardCache.results[code]||null;
}
function getAllCardCodes(){
if(!cardCache||!cardCache.results)return[];
return Object.keys(cardCache.results);
}
function getClanInfo(clanId){
if(!cardCache||!cardCache.clans)return null;
return cardCache.clans[clanId]||null;
}
function rCG(){
const wrap=document.getElementById('homeCardGallery');
const btn=document.getElementById('homeGachaBtn');
const cnt=document.getElementById('cardCountText');
const scroll=document.getElementById('cardGalleryScroll');
if(!wrap||!scroll)return;
const owned=cardState.owned;
if(cnt)cnt.textContent=owned.length+'/16';
if(owned.length===0){
if(!localStorage.getItem('mathbrain_pet')){
scroll.innerHTML='<div class="card-gallery-empty"><span class="cg-empty-icon">🏮</span>去做MathBTI<br>领取你的第一张数学人物小卡</div>';
}else{
scroll.innerHTML='<div class="card-gallery-empty"><span class="cg-empty-icon">🏮</span>还没获得卡片<br>去知识卡片页阅读攒抽卡机会吧</div>';
}
wrap.style.display='block';
if(btn){
if(cardState.draws>0){
btn.style.display='inline-flex';
document.getElementById('homeDrawCount').textContent=cardState.draws;
}else{
btn.style.display='none';
}
}
return;
}
wrap.style.display='block';
loadCardData().then(()=>{
let h='';
owned.forEach(code=>{
const info=getCardInfo(code);
const clan=info?getClanInfo(info.clan):null;
const clanColor=clan?clan.color:'#A8907E';
h+='<div class="card-gallery-item" onclick="openCardDetail(\''+code+'\')" style="--clan-color:'+clanColor+'">'+
'<div class="cg-icon" style="background:'+(info?info.icon_bg:'#F0F0F0')+'">'+(info?info.icon:'?')+'</div>'+
'<div class="cg-name" title="'+(info?info.name:'')+'">'+(info?info.name:'')+'</div></div>';
});
scroll.innerHTML=h;
});
if(btn){
if(cardState.draws>0){
btn.style.display='inline-flex';
document.getElementById('homeDrawCount').textContent=cardState.draws;
}else{
btn.style.display='none';
}
}
}
function openGacha(){
document.getElementById('gachaOverlay').style.display='flex';
document.getElementById('gachaIdle').style.display='block';
document.getElementById('gachaResult').style.display='none';
document.getElementById('gachaDrawCount').textContent=cardState.draws;
const all=cardData?Object.keys(cardData.results).length:16;
const missing=all-cardState.owned.length;
let pityHint='';
if(cardState.drawHistory.length>0){
const sinceNew=cardState.drawHistory.slice().reverse().findIndex(d=>cardState.owned.indexOf(d.code)===-1);
if(sinceNew>=4)pityHint='⭐ 下次抽卡必出新卡!';
else if(sinceNew>=0)pityHint='再抽'+(4-sinceNew)+'次保底出新卡';
}
document.getElementById('gachaPityHint').textContent=pityHint||'剩余未收集:'+missing+'/16';
}
function closeGacha(){document.getElementById('gachaOverlay').style.display='none'}
function doDraw(){
if(_gachaBusy)return;
if(cardState.draws<=0){showToast('没有抽卡次数了!去读知识卡片获取吧');return}
_gachaBusy=true;
const gourd=document.getElementById('gachaGourd');
gourd.classList.add('gourd-shaking');
document.getElementById('gachaIdle').style.display='block';
document.getElementById('gachaResult').style.display='none';
setTimeout(()=>{
gourd.classList.remove('gourd-shaking');
cardState.draws--;
saveCardState();
let code;
const allCodes=getAllCardCodes();
const sinceNewIndex=cardState.drawHistory.slice().reverse().findIndex(d=>!cardState.owned.includes(d.code));
if(sinceNewIndex>=4||(cardState.drawHistory.length>0&&sinceNewIndex===-1)){
const missing2=allCodes.filter(c=>!cardState.owned.includes(c));
code=missing2[Math.floor(Math.random()*missing2.length)];
}else{
code=allCodes[Math.floor(Math.random()*allCodes.length)];
}
const isNew=cardState.owned.indexOf(code)===-1;
if(isNew){
cardState.owned.push(code);
}
cardState.drawHistory.push({code:code,date:new Date().toISOString().slice(0,10),method:'gacha'});
saveCardState();
showDrawResult(code,isNew);
const cr=document.querySelector('.gacha-card-reveal');
if(cr)setTimeout(()=>burstGachaConfetti(cr),300);
_gachaBusy=false;
rCG();
},900);
}
function showDrawResult(code,isNew){
loadCardData().then(()=>{
const info=getCardInfo(code);
const clan=info?getClanInfo(info.clan):null;
const clanColor=clan?clan.color:'#A8907E';
document.getElementById('gachaIdle').style.display='none';
const res=document.getElementById('gachaResult');
res.style.display='block';
res.innerHTML=
'<div class="gacha-card-reveal card-flip-in" style="--clan-color:'+clanColor+'">'+
(info&&info.portrait
?'<div class="gacha-card-portrait"><img src="'+esc(info.portrait)+'" alt="" onerror="this.style.display=\'none\';this.nextElementSibling.style.display=\'flex\'" style="width:100%;height:100%;object-fit:cover"><span style="display:none;font-size:48px">'+(info.icon||'?')+'</span></div>'
:'<div class="gacha-card-portrait"><span style="font-size:48px">'+(info?info.icon:'?')+'</span></div>')+
'<div class="gacha-card-name">'+(info?info.name:'???')+'</div>'+
'<div class="gacha-card-type">'+code+'</div>'+
(clan?'<div class="gacha-card-clan" style="background:'+clanColor+'15;color:'+clanColor+';border:1px solid '+clanColor+'30">'+clan.emoji+' '+clan.name+' · '+(info?info.mathematician:'')+'</div>':'')+
'<div class="gacha-card-tagline">'+(info?info.tagline:'')+'</div>'+
(isNew?'':'<div class="gacha-card-dupe">(重复获得 · 已自动分解)</div>')+
'<button class="gacha-card-detail-btn" onclick="openCardDetail(\''+code+'\')">📋 查看卡片详情</button>'+
'<div style="margin-top:12px"><button class="draw-btn" onclick="openGacha();doDraw()" style="font-size:12px;padding:8px 20px"'+(cardState.draws<=0?' disabled':'')+'>再抽一次 (剩余'+cardState.draws+'次)</button></div>'+
'</div>';
document.getElementById('gachaDrawCount').textContent=cardState.draws;
});
}
function openCardDetail(code){
loadCardData().then(()=>{
const info=getCardInfo(code);
if(!info)return;
const clan=getClanInfo(info.clan);
const clanColor=clan?clan.color:'#A8907E';
const hist=cardState.drawHistory.filter(d=>d.code===code);
const obtainInfo=hist.length>0
?(hist[0].method==='mathbti'?'📋 MathBTI测试获得':'🏮 葫芦抽卡获得')+' · '+hist[0].date
:'未知途径';
const modal=document.getElementById('cardDetailModal');
modal.innerHTML=
'<div class="card-detail-header">'+
'<div class="card-detail-bg" style="background:'+clanColor+'"></div>'+
'<button class="card-detail-close" onclick="closeCardDetail()">✕</button>'+
(info.portrait
?'<div class="card-detail-portrait"><img src="'+esc(info.portrait)+'" alt="" onerror="this.style.display=\'none\'"><span style="font-size:40px;display:none">'+(info.icon||'?')+'</span></div>'
:'<div class="card-detail-portrait"><span style="font-size:40px">'+(info.icon||'?')+'</span></div>')+
'<div class="card-detail-name">'+esc(info.name)+'</div>'+
'<div class="card-detail-binary">'+code+'</div>'+
(clan?'<div class="card-detail-clan" style="background:'+clanColor+'15;color:'+clanColor+';border:1px solid '+clanColor+'30">'+clan.emoji+' '+clan.name+' · '+esc(info.mathematician)+'</div>':'')+
'<div class="card-detail-tagline">'+esc(info.tagline||'')+'</div>'+
'</div>'+
'<div class="card-detail-body">'+
'<div class="card-detail-section"><div class="card-detail-section-title">人物简介</div><p>'+esc(info.description||'')+'</p></div>'+
'<div class="card-detail-section"><div class="card-detail-section-title">代表数学家 · '+esc(info.mathematician)+'</div><p>'+esc(info.bio||'')+'</p></div>'+
'<div class="card-detail-fact"><span class="fact-emoji">💡</span><span class="fact-text">'+esc(info.bio_fact||'')+'</span></div>'+
'<div class="card-detail-obtain"><span class="obtain-icon">🎴</span><span>'+obtainInfo+'</span></div>'+
'</div>';
document.getElementById('cardDetailOverlay').style.display='flex';
});
}
function closeCardDetail(){document.getElementById('cardDetailOverlay').style.display='none'}
function burstGachaConfetti(el){
if(!el)return;const r=el.getBoundingClientRect();const cx=r.left+r.width/2,cy=r.top+r.height/2;
const colors=['#A8907E','#C47F3A','#D4A574','#8A9BA8','#C49595','#8AA68A'];
for(let i=0;i<18;i++){const p=document.createElement('div');p.className='gacha-particle';const a=Math.random()*Math.PI*2,d=40+Math.random()*60;p.style.cssText='left:'+cx+'px;top:'+cy+'px;--gpx:'+(Math.cos(a)*d)+'px;--gpy:'+(Math.sin(a)*d)+'px;background:'+colors[Math.floor(Math.random()*colors.length)];document.body.appendChild(p);setTimeout(()=>p.remove(),1100)}
}
function showToast(msg){
let c=document.getElementById('toastBox');if(!c){c=document.createElement('div');c.id='toastBox';c.className='toast-container';document.body.appendChild(c)}
const t=document.createElement('div');t.className='toast';t.innerHTML=msg;c.appendChild(t);setTimeout(()=>t.remove(),2400);
}
function burstConfetti(el){
if(!el)return;const r=el.getBoundingClientRect();const cx=r.left+r.width/2,cy=r.top+r.height/2;
const colors=['#808C95','#A8907E','#8AA68A','#C4A467','#C49595','#9AA5AE'];
for(let i=0;i<14;i++){const p=document.createElement('div');p.className='particle';const a=Math.random()*Math.PI*2,d=30+Math.random()*50;p.style.cssText='left:'+cx+'px;top:'+cy+'px;width:6px;height:6px;border-radius:2px;background:'+colors[Math.floor(Math.random()*colors.length)]+';--px:'+(Math.cos(a)*d)+'px;--py:'+(Math.sin(a)*d)+'px';document.body.appendChild(p);setTimeout(()=>p.remove(),750)}
}
function renderSkeletons(cid,count){
const c=document.getElementById(cid);c.innerHTML='';
for(let i=0;i<count;i++){c.insertAdjacentHTML('beforeend','<div class="skeleton-card"><div class="skeleton skeleton-line w80"></div><div class="skeleton skeleton-line w60"></div><div class="skeleton skeleton-line w40"></div><div style="display:flex;align-items:center;gap:8px;margin-top:auto"><div class="skeleton skeleton-avatar"></div><div class="skeleton skeleton-line" style="width:50%;height:10px"></div></div></div>')}
}
let favP=null;
document.addEventListener('DOMContentLoaded',()=>{
document.querySelectorAll('.lang-btn').forEach(b=>b.classList.toggle('active',b.dataset.lang===_lang));
document.getElementById('globalSearch').placeholder=t('search');
rI18n();
loadHome();
initCardState();
loadCardData();
setTimeout(rCG,300);
favP=rFC();
api('/api/videos?module=数人').then(r=>{if(r.code===0){allV=r.data||[];vD=r.disciplines||[]}}).catch(()=>{});
api('/api/knowledge').then(r=>{if(r.code===0){allK=r.data||[];kT=r.topics||[]}}).catch(()=>{});
api('/api/user/profile?user_id='+USER_ID).then(r=>{if(r.code===0&&r.data){document.getElementById('sidebarName').textContent=r.data.nickname||'游客用户';document.getElementById('sidebarAvatar').textContent=r.data.avatar_emoji||'葫';document.getElementById('sidebarAvatar').style.background=r.data.avatar_color||'#808C95'}}).catch(()=>{});
});
+447
View File
@@ -0,0 +1,447 @@
// MathBTI 前端逻辑 — 12题 → 16结果 → 4族
let testData = null, answers = [], currentQ = 0, myResult = null;
async function init() {
try {
const r = await fetch('/data/seed_mathbti.json');
testData = await r.json();
renderIntro();
} catch (e) {
document.getElementById('introScreen').innerHTML =
'<div style="text-align:center;padding:60px 20px"><p style="color:#94A3B8;margin-bottom:12px">加载失败,请检查网络</p><button class="btn-start" onclick="init()">重试</button></div>';
}
}
function renderIntro() {
const axesEl = document.getElementById('introAxes');
if (axesEl && testData.axes) {
axesEl.innerHTML = testData.axes.map(a =>
'<div class="intro-axis-item">' +
'<span class="axis-emoji">' + (a.emoji_left || '') + '</span>' +
'<span class="axis-label">' + a.name + '</span>' +
'<span class="axis-emoji">' + (a.emoji_right || '') + '</span>' +
'</div>'
).join('');
}
document.querySelector('.intro-meta').textContent =
'⏱ 约2分钟 · 4选项 · 16种人格';
}
function startQuiz() {
answers = new Array(testData.questions.length).fill(-1);
currentQ = 0;
switchScreen('quizScreen');
renderQuestion();
}
function renderQuestion() {
const q = testData.questions[currentQ];
document.getElementById('quizQuestion').textContent = q.question;
const optEl = document.getElementById('quizOptions');
const sel = answers[currentQ];
optEl.innerHTML = q.options.map((o, i) =>
'<label class="quiz-option' + (sel === i ? ' selected' : '') + '" onclick="selectOption(' + i + ')">' +
'<input type="radio" name="q' + currentQ + '" value="' + i + '"' + (sel === i ? ' checked' : '') + '>' +
'<span class="opt-text">' + o.text + '</span></label>'
).join('');
// Progress
const pct = Math.round((currentQ + 1) / testData.questions.length * 100);
document.getElementById('progressFill').style.width = pct + '%';
// 最后一题时进度条末尾显示葫芦图标
const isLast=currentQ===testData.questions.length-1;
document.getElementById('progressFill').style.setProperty('--is-last',isLast?'1':'0');
document.getElementById('progressText').textContent = (currentQ + 1) + '/' + testData.questions.length;
// Next button
const btn = document.getElementById('btnNext');
btn.disabled = answers[currentQ] === -1;
btn.textContent = currentQ === testData.questions.length - 1 ? '查看结果' : '下一题';
}
function selectOption(idx) {
answers[currentQ] = idx;
document.getElementById('btnNext').disabled = false;
// Update visual
document.querySelectorAll('.quiz-option').forEach((el, i) => {
el.classList.toggle('selected', i === idx);
});
}
async function nextQuestion() {
if (answers[currentQ] === -1) return;
if (currentQ < testData.questions.length - 1) {
currentQ++;
renderQuestion();
return;
}
// Last question → calculate locally
const btn = document.getElementById('btnNext');
btn.disabled = true;
btn.textContent = '计算中…';
try {
myResult = calculateResult(answers);
// 自动获得对应的数学人物卡
const mbtiCode=myResult.binary_code;
let gotNewCard=false;
if(mbtiCode){
let cardState2=JSON.parse(localStorage.getItem('hulu_card_state')||'{}');
if(!cardState2.owned)cardState2.owned=[];
if(!cardState2.drawHistory)cardState2.drawHistory=[];
if(cardState2.owned.indexOf(mbtiCode)===-1){
cardState2.owned.push(mbtiCode);
cardState2.drawHistory.push({code:mbtiCode,date:new Date().toISOString().slice(0,10),method:'mathbti'});
localStorage.setItem('hulu_card_state',JSON.stringify(cardState2));
gotNewCard=true;
}
}
renderResult();
// 卡片领取提示
if(gotNewCard){
const res=myResult.result;
const notice=document.getElementById('cardNotice');
if(notice){notice.style.display='block';document.getElementById('cardNoticeName').textContent=res?res.name:'—'}
showToast('🎴 获得数学人物小卡!前往葫芦数学查看卡册');
}
switchScreen('resultScreen');
} catch (e) {
showToast('提交失败,请重试');
btn.disabled = false;
btn.textContent = currentQ === testData.questions.length - 1 ? '查看结果' : '下一题';
}
}
function calculateResult(answers) {
const axes = testData.scoring.axes; // ["style","purpose","era","social"]
const maxPerAxis = testData.scoring.max_per_axis;
const cutoff = testData.scoring.cutoff;
// 计算每个轴的得分
const axisScores = {};
axes.forEach(axis => {
axisScores[axis] = 0;
});
testData.questions.forEach((q, i) => {
const score = q.options[answers[i]].score;
axisScores[q.axis] += score;
});
// 生成二进制代码 (≤cutoff=0左极, ≥cutoff+1=1右极)
const binaryCode = axes.map(axis => axisScores[axis] <= cutoff ? '0' : '1').join('');
// 获取结果
const result = testData.results[binaryCode];
return {
binary_code: binaryCode,
axis_scores: axisScores,
result: result
};
}
function renderResult() {
const res = myResult.result;
if (!res) return;
// Portrait
const portraitEl = document.getElementById('resultPortrait');
if (res.portrait) {
portraitEl.innerHTML = '<div class="result-portrait" style="background:' + (res.icon_bg || '#F0F0F0') + '"><img src="' + res.portrait + '" alt="" style="width:100%;height:100%;object-fit:cover;border-radius:50%" onerror="this.style.display=\'none\';this.nextElementSibling.style.display=\'flex\'"><span class="result-icon-fallback" style="display:none;font-size:48px">' + (res.icon || 'Σ') + '</span></div>';
} else {
portraitEl.innerHTML = '<div class="result-portrait" style="background:' + (res.icon_bg || '#F0F0F0') + '"><span class="result-icon-fallback" style="font-size:48px">' + (res.icon || 'Σ') + '</span></div>';
}
// Name + Clan
document.getElementById('resultName').innerHTML = res.name + ' <span class="type-code" style="font-size:13px;color:#94A3B8;font-weight:400">' + myResult.binary_code + '</span>';
document.getElementById('resultTagline').textContent = res.tagline || '';
// Clan badge
const clanData = testData.clans[res.clan];
document.getElementById('resultMathematician').innerHTML =
(clanData ? '<span class="clan-badge" style="background:' + (clanData.color || '#888') + '15;color:' + (clanData.color || '#888') + ';border:1px solid ' + (clanData.color || '#888') + '30">' + clanData.emoji + ' ' + clanData.name + ' · ' + res.mathematician + '</span>' : '') +
'<span class="mathematician-name">代表数学家:' + (res.mathematician || '—') + '</span>';
// Keywords
const kwEl = document.getElementById('resultKeywords');
if (res.keywords) {
kwEl.innerHTML = res.keywords.map(k => '<span class="kw-tag">' + k + '</span>').join('');
kwEl.style.display = 'flex';
} else {
kwEl.style.display = 'none';
}
// Description
document.getElementById('resultDesc').textContent = res.description || '';
document.getElementById('resultShort').textContent = res.short_desc || '';
// Bio
document.getElementById('bioTitle').textContent = '关于 ' + (res.mathematician || '这位数学家');
document.getElementById('bioText').textContent = res.bio || '';
document.getElementById('bioFact').textContent = res.bio_fact || '';
// Axes bars
const axesEl = document.getElementById('resultAxes');
if (testData.axes) {
axesEl.innerHTML = testData.axes.map(a => {
const score = myResult.axis_scores[a.id] || 0;
const maxScore = testData.scoring.max_per_axis || 9;
const pct = Math.round(score / maxScore * 100);
return '<div class="axis-bar-row">' +
'<span class="axis-bar-label">' + (a.emoji_left || '') + ' ' + a.left + '</span>' +
'<div class="axis-bar-track"><div class="axis-bar-fill" style="width:' + pct + '%"></div></div>' +
'<span class="axis-bar-label">' + a.right + ' ' + (a.emoji_right || '') + '</span>' +
'</div>';
}).join('');
axesEl.style.display = 'block';
}
// Five-dimension ability card from 16位数学家五维能力表
const s5El = document.getElementById('resultStats5');
if (s5El && res.stats5) {
const labels = ['眼光', '人文', '侦探', '建模', '联结'];
s5El.innerHTML = '<div class="result-stats5-title">五维能力</div>' + labels.map(k => {
const v = res.stats5[k] || 0;
return '<div class="axis-bar-row">' +
'<span class="axis-bar-label">' + k + '</span>' +
'<div class="axis-bar-track"><div class="axis-bar-fill" style="width:' + v + '%"></div></div>' +
'<span class="axis-bar-label">' + v + '</span>' +
'</div>';
}).join('');
s5El.style.display = 'block';
}
// Adopt pet
const petData = JSON.parse(localStorage.getItem('mb_pet') || '{}');
const adoptBtn = document.querySelector('.btn-adopt');
if (petData.type) {
adoptBtn.textContent = '✅ 已领养 · 我的数学精灵';
adoptBtn.classList.add('adopted');
}
// Apply clan color
if (clanData) {
document.getElementById('resultScreen').style.setProperty('--clan-color', clanData.color);
}
}
// ---- Bio Expander ----
function toggleBio() {
const body = document.getElementById('bioBody');
const arrow = document.getElementById('bioArrow');
const expanded = body.style.maxHeight && body.style.maxHeight !== '0px';
if (expanded) {
body.style.maxHeight = '0px';
arrow.textContent = '▾';
} else {
body.style.maxHeight = body.scrollHeight + 'px';
arrow.textContent = '▴';
}
}
// ---- Adopt Pet ----
function adoptPet() {
if (!myResult) return;
const res = myResult.result;
const dims = {};
['眼光', '人文', '侦探', '建模', '联结'].forEach(k => {
dims[k] = Math.max(10, Math.min(100, Number((res.stats5 && res.stats5[k]) || 0)));
});
const pet = {
name: res.name || '—',
type: res.name || '—',
icon: res.icon || 'Σ',
color: res.color || '#5B6AF0',
mathematician: res.mathematician || '',
clan: res.clan || '',
binary_code: myResult.binary_code,
level: 1,
exp: 0,
dims: dims
};
localStorage.setItem('mb_pet', JSON.stringify(pet));
localStorage.setItem('mathbrain_pet', JSON.stringify(pet));
const btn = document.querySelector('.btn-adopt');
btn.textContent = '✅ 已领养 · 我的数学精灵';
btn.classList.add('adopted');
showToast('🧬 数学精灵已领养!每天来葫芦数学 App 喂它成长');
}
// ---- Share ----
function copyShare() {
if (!myResult) return;
const res = myResult.result;
const clanData = testData.clans[res.clan];
const text = '我的数学人格是「' + res.name + '」' + (res.icon || '') + '\n' +
(clanData ? clanData.emoji + ' ' + clanData.name + ' · ' + res.mathematician : '') + '\n' +
(res.short_desc || '') + '\n\n' +
'👉 你也来测测:' + window.location.href + '\n\n' +
'#数学人格测试 #MathBTI';
navigator.clipboard.writeText(text).then(() => showToast('已复制分享文案!'));
}
function shareResult() {
if (!myResult) return;
const res = myResult.result;
const text = '我的数学人格是「' + res.name + '」' + (res.icon || '') + '' + (res.short_desc || '');
if (navigator.share) {
navigator.share({ title: 'MathBTI · 数学人格测试', text: text, url: window.location.href }).catch(() => {});
} else {
copyShare();
}
}
function generateShareCard() {
if (!myResult) return;
const res = myResult.result;
const clanData = testData.clans[res.clan];
const canvas = document.getElementById('shareCardCanvas');
const ctx = canvas.getContext('2d');
canvas.width = 600;
canvas.height = 800;
// Background
const gradient = ctx.createLinearGradient(0, 0, 600, 800);
gradient.addColorStop(0, (res.color || '#5B6AF0') + '22');
gradient.addColorStop(1, (res.color || '#5B6AF0') + '08');
ctx.fillStyle = '#FAFBFC';
ctx.fillRect(0, 0, 600, 800);
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, 600, 800);
// Top accent bar
ctx.fillStyle = res.color || '#5B6AF0';
ctx.fillRect(0, 0, 600, 6);
// Portrait placeholder
ctx.fillStyle = res.icon_bg || '#F0F0F0';
ctx.beginPath();
ctx.arc(300, 180, 70, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#334155';
ctx.font = '60px sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(res.icon || 'Σ', 300, 180);
// Name
ctx.fillStyle = '#1E293B';
ctx.font = 'bold 36px "PingFang SC", sans-serif';
ctx.fillText(res.name, 300, 280);
// Clan + Mathematician
ctx.fillStyle = '#64748B';
ctx.font = '18px "PingFang SC", sans-serif';
if (clanData) {
ctx.fillText(clanData.emoji + ' ' + clanData.name + ' · ' + res.mathematician, 300, 315);
}
// Tagline
ctx.fillStyle = res.color || '#5B6AF0';
ctx.font = 'italic 15px "PingFang SC", sans-serif';
const tagline = res.tagline || '';
wrapText(ctx, tagline, 300, 350, 480, 22);
// Separator
ctx.fillStyle = '#E2E8F0';
ctx.fillRect(60, 380, 480, 1);
// Short desc
ctx.fillStyle = '#334155';
ctx.font = '15px "PingFang SC", sans-serif';
const desc = res.short_desc || '';
wrapText(ctx, desc, 300, 410, 480, 24);
// Axes
let ay = 450;
if (testData.axes && myResult.axis_scores) {
ctx.fillStyle = '#64748B';
ctx.font = '14px "PingFang SC", sans-serif';
testData.axes.forEach(a => {
const s = myResult.axis_scores[a.id] || 0;
const maxS = testData.scoring.max_per_axis || 9;
const pct = Math.round(s / maxS * 100);
ctx.fillText(a.emoji_left + ' ' + a.left + ' — ' + pct + '% — ' + a.right + ' ' + a.emoji_right, 300, ay);
ay += 28;
});
}
// Bottom
ctx.fillStyle = '#94A3B8';
ctx.font = '13px "PingFang SC", sans-serif';
ctx.fillText('葫芦数学 · MathBTI', 300, 730);
// 葫芦水印
ctx.save();ctx.globalAlpha=.06;ctx.font='80px sans-serif';ctx.fillStyle='#A8907E';
ctx.fillText('🏮',480,680);
ctx.restore();
ctx.fillText('你也来测测你的数学人格类型 →', 300, 755);
// QR placeholder
ctx.fillStyle = '#E2E8F0';
ctx.fillRect(255, 640, 90, 90);
ctx.fillStyle = '#94A3B8';
ctx.font = '11px sans-serif';
ctx.fillText('扫码测', 300, 685);
document.getElementById('shareCardPreview').style.display = 'block';
document.getElementById('shareCardPreview').scrollIntoView({ behavior: 'smooth' });
}
function downloadShareCard() {
const canvas = document.getElementById('shareCardCanvas');
const link = document.createElement('a');
link.download = 'mathbti_' + (myResult ? myResult.binary_code : 'result') + '.png';
link.href = canvas.toDataURL('image/png');
link.click();
showToast('📸 图片已保存!');
}
function wrapText(ctx, text, x, y, maxWidth, lineHeight) {
const lines = [];
let current = '';
for (let i = 0; i < text.length; i++) {
const test = current + text[i];
if (ctx.measureText(test).width > maxWidth && current.length > 0) {
lines.push(current);
current = text[i];
} else {
current = test;
}
}
if (current) lines.push(current);
lines.forEach((line, idx) => {
ctx.fillText(line, x, y + idx * lineHeight);
});
}
// ---- Retry ----
function retryQuiz() {
answers = [];
currentQ = 0;
myResult = null;
document.getElementById('bioBody').style.maxHeight = '0px';
document.getElementById('bioArrow').textContent = '▾';
document.getElementById('shareCardPreview').style.display = 'none';
switchScreen('introScreen');
}
// ---- Helpers ----
function switchScreen(id) {
document.querySelectorAll('.screen').forEach(s => s.classList.remove('active'));
document.getElementById(id).classList.add('active');
window.scrollTo({ top: 0, behavior: 'smooth' });
}
let toastTimer = null;
function showToast(msg) {
const el = document.getElementById('toast');
el.textContent = msg;
el.classList.add('show');
clearTimeout(toastTimer);
toastTimer = setTimeout(() => el.classList.remove('show'), 2200);
}
// Bootstrap
document.addEventListener('DOMContentLoaded', init);
+325
View File
@@ -0,0 +1,325 @@
let testData = null, answers = [], currentQ = 0, myResult = null;
async function init() {
try {
const r = await fetch('/api/math_personality');
const j = await r.json();
if (j.code !== 0) { showToast('测试数据加载失败'); return; }
testData = j.data;
renderIntro();
} catch (e) {
document.getElementById('introScreen').innerHTML =
'<div style="text-align:center;padding:60px 20px"><p style="color:#94A3B8;margin-bottom:12px">加载失败,请检查网络</p><button class="btn-start" onclick="init()">重试</button></div>';
}
}
function renderIntro() {
const axesEl = document.getElementById('introAxes');
if (axesEl && testData.axes) {
axesEl.innerHTML = testData.axes.map(a =>
'<div class="intro-axis-item">' +
'<span class="axis-emoji">' + (a.emoji_left || '') + '</span>' +
'<span class="axis-label">' + a.name + '</span>' +
'<span class="axis-emoji">' + (a.emoji_right || '') + '</span>' +
'</div>'
).join('');
}
document.querySelector('.intro-meta').textContent =
'⏱ 约2分钟 · 4选项 · 16种人格';
}
function startQuiz() {
answers = new Array(testData.questions.length).fill(-1);
currentQ = 0;
switchScreen('quizScreen');
renderQuestion();
}
function renderQuestion() {
const q = testData.questions[currentQ];
document.getElementById('quizQuestion').textContent = q.question;
const optEl = document.getElementById('quizOptions');
const sel = answers[currentQ];
optEl.innerHTML = q.options.map((o, i) =>
'<label class="quiz-option' + (sel === i ? ' selected' : '') + '" onclick="selectOption(' + i + ')">' +
'<input type="radio" name="q' + currentQ + '" value="' + i + '"' + (sel === i ? ' checked' : '') + '>' +
'<span class="opt-text">' + o.text + '</span></label>'
).join('');
const pct = Math.round((currentQ + 1) / testData.questions.length * 100);
document.getElementById('progressFill').style.width = pct + '%';
const isLast=currentQ===testData.questions.length-1;
document.getElementById('progressFill').style.setProperty('--is-last',isLast?'1':'0');
document.getElementById('progressText').textContent = (currentQ + 1) + '/' + testData.questions.length;
const btn = document.getElementById('btnNext');
btn.disabled = answers[currentQ] === -1;
btn.textContent = currentQ === testData.questions.length - 1 ? '查看结果' : '下一题';
}
function selectOption(idx) {
answers[currentQ] = idx;
document.getElementById('btnNext').disabled = false;
document.querySelectorAll('.quiz-option').forEach((el, i) => {
el.classList.toggle('selected', i === idx);
});
}
async function nextQuestion() {
if (answers[currentQ] === -1) return;
if (currentQ < testData.questions.length - 1) {
currentQ++;
renderQuestion();
return;
}
const btn = document.getElementById('btnNext');
btn.disabled = true;
btn.textContent = '计算中…';
try {
const r = await fetch('/api/math_personality/calculate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ answers })
});
const j = await r.json();
if (j.code !== 0) { showToast('计算结果失败'); return; }
myResult = j.data;
const mbtiCode=j.data.binary_code;
let gotNewCard=false;
if(mbtiCode){
let cardState2=JSON.parse(localStorage.getItem('hulu_card_state')||'{}');
if(!cardState2.owned)cardState2.owned=[];
if(!cardState2.drawHistory)cardState2.drawHistory=[];
if(cardState2.owned.indexOf(mbtiCode)===-1){
cardState2.owned.push(mbtiCode);
cardState2.drawHistory.push({code:mbtiCode,date:new Date().toISOString().slice(0,10),method:'mathbti'});
localStorage.setItem('hulu_card_state',JSON.stringify(cardState2));
gotNewCard=true;
}
}
renderResult();
if(gotNewCard){
const res=j.data.result;
const notice=document.getElementById('cardNotice');
if(notice){notice.style.display='block';document.getElementById('cardNoticeName').textContent=res?res.name:'—'}
showToast('🎴 获得数学人物小卡!前往葫芦数学查看卡册');
}
switchScreen('resultScreen');
} catch (e) {
showToast('提交失败,请重试');
btn.disabled = false;
btn.textContent = currentQ === testData.questions.length - 1 ? '查看结果' : '下一题';
}
}
function renderResult() {
const res = myResult.result;
if (!res) return;
const portraitEl = document.getElementById('resultPortrait');
if (res.portrait) {
portraitEl.innerHTML = '<div class="result-portrait" style="background:' + (res.icon_bg || '#F0F0F0') + '"><img src="' + res.portrait + '" alt="" style="width:100%;height:100%;object-fit:cover;border-radius:50%" onerror="this.style.display=\'none\';this.nextElementSibling.style.display=\'flex\'"><span class="result-icon-fallback" style="display:none;font-size:48px">' + (res.icon || 'Σ') + '</span></div>';
} else {
portraitEl.innerHTML = '<div class="result-portrait" style="background:' + (res.icon_bg || '#F0F0F0') + '"><span class="result-icon-fallback" style="font-size:48px">' + (res.icon || 'Σ') + '</span></div>';
}
document.getElementById('resultName').innerHTML = res.name + ' <span class="type-code" style="font-size:13px;color:#94A3B8;font-weight:400">' + myResult.binary_code + '</span>';
document.getElementById('resultTagline').textContent = res.tagline || '';
const clanData = testData.clans[res.clan];
document.getElementById('resultMathematician').innerHTML =
(clanData ? '<span class="clan-badge" style="background:' + (clanData.color || '#888') + '15;color:' + (clanData.color || '#888') + ';border:1px solid ' + (clanData.color || '#888') + '30">' + clanData.emoji + ' ' + clanData.name + ' · ' + res.mathematician + '</span>' : '') +
'<span class="mathematician-name">代表数学家:' + (res.mathematician || '—') + '</span>';
const kwEl = document.getElementById('resultKeywords');
if (res.keywords) {
kwEl.innerHTML = res.keywords.map(k => '<span class="kw-tag">' + k + '</span>').join('');
kwEl.style.display = 'flex';
} else {
kwEl.style.display = 'none';
}
document.getElementById('resultDesc').textContent = res.description || '';
document.getElementById('resultShort').textContent = res.short_desc || '';
document.getElementById('bioTitle').textContent = '关于 ' + (res.mathematician || '这位数学家');
document.getElementById('bioText').textContent = res.bio || '';
document.getElementById('bioFact').textContent = res.bio_fact || '';
const axesEl = document.getElementById('resultAxes');
if (testData.axes) {
axesEl.innerHTML = testData.axes.map(a => {
const score = myResult.axis_scores[a.id] || 0;
const maxScore = testData.scoring.max_per_axis || 9;
const pct = Math.round(score / maxScore * 100);
return '<div class="axis-bar-row">' +
'<span class="axis-bar-label">' + (a.emoji_left || '') + ' ' + a.left + '</span>' +
'<div class="axis-bar-track"><div class="axis-bar-fill" style="width:' + pct + '%"></div></div>' +
'<span class="axis-bar-label">' + a.right + ' ' + (a.emoji_right || '') + '</span>' +
'</div>';
}).join('');
axesEl.style.display = 'block';
}
const petData = JSON.parse(localStorage.getItem('mb_pet') || '{}');
const adoptBtn = document.querySelector('.btn-adopt');
if (petData.type) {
adoptBtn.textContent = '✅ 已领养 · 我的数学精灵';
adoptBtn.classList.add('adopted');
}
if (clanData) {
document.getElementById('resultScreen').style.setProperty('--clan-color', clanData.color);
}
}
function toggleBio() {
const body = document.getElementById('bioBody');
const arrow = document.getElementById('bioArrow');
const expanded = body.style.maxHeight && body.style.maxHeight !== '0px';
if (expanded) {
body.style.maxHeight = '0px';
arrow.textContent = '▾';
} else {
body.style.maxHeight = body.scrollHeight + 'px';
arrow.textContent = '▴';
}
}
function adoptPet() {
if (!myResult) return;
const res = myResult.result;
const pet = {
type: res.name || '—',
icon: res.icon || 'Σ',
color: res.color || '#5B6AF0',
mathematician: res.mathematician || '',
clan: res.clan || '',
binary_code: myResult.binary_code
};
localStorage.setItem('mb_pet', JSON.stringify(pet));
const btn = document.querySelector('.btn-adopt');
btn.textContent = '✅ 已领养 · 我的数学精灵';
btn.classList.add('adopted');
showToast('🧬 数学精灵已领养!每天来葫芦数学 App 喂它成长');
}
function copyShare() {
if (!myResult) return;
const res = myResult.result;
const clanData = testData.clans[res.clan];
const text = '我的数学人格是「' + res.name + '」' + (res.icon || '') + '\n' +
(clanData ? clanData.emoji + ' ' + clanData.name + ' · ' + res.mathematician : '') + '\n' +
(res.short_desc || '') + '\n\n' +
'👉 你也来测测:' + window.location.href + '\n\n' +
'#数学人格测试 #MathBTI';
navigator.clipboard.writeText(text).then(() => showToast('已复制分享文案!'));
}
function shareResult() {
if (!myResult) return;
const res = myResult.result;
const text = '我的数学人格是「' + res.name + '」' + (res.icon || '') + '' + (res.short_desc || '');
if (navigator.share) {
navigator.share({ title: 'MathBTI · 数学人格测试', text: text, url: window.location.href }).catch(() => {});
} else {
copyShare();
}
}
function generateShareCard() {
if (!myResult) return;
const res = myResult.result;
const clanData = testData.clans[res.clan];
const canvas = document.getElementById('shareCardCanvas');
const ctx = canvas.getContext('2d');
canvas.width = 600;
canvas.height = 800;
const gradient = ctx.createLinearGradient(0, 0, 600, 800);
gradient.addColorStop(0, (res.color || '#5B6AF0') + '22');
gradient.addColorStop(1, (res.color || '#5B6AF0') + '08');
ctx.fillStyle = '#FAFBFC';
ctx.fillRect(0, 0, 600, 800);
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, 600, 800);
ctx.fillStyle = res.color || '#5B6AF0';
ctx.fillRect(0, 0, 600, 6);
ctx.fillStyle = res.icon_bg || '#F0F0F0';
ctx.beginPath();
ctx.arc(300, 180, 70, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#334155';
ctx.font = '60px sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(res.icon || 'Σ', 300, 180);
ctx.fillStyle = '#1E293B';
ctx.font = 'bold 36px "PingFang SC", sans-serif';
ctx.fillText(res.name, 300, 280);
ctx.fillStyle = '#64748B';
ctx.font = '18px "PingFang SC", sans-serif';
if (clanData) {
ctx.fillText(clanData.emoji + ' ' + clanData.name + ' · ' + res.mathematician, 300, 315);
}
ctx.fillStyle = res.color || '#5B6AF0';
ctx.font = 'italic 15px "PingFang SC", sans-serif';
const tagline = res.tagline || '';
wrapText(ctx, tagline, 300, 350, 480, 22);
ctx.fillStyle = '#E2E8F0';
ctx.fillRect(60, 380, 480, 1);
ctx.fillStyle = '#334155';
ctx.font = '15px "PingFang SC", sans-serif';
const desc = res.short_desc || '';
wrapText(ctx, desc, 300, 410, 480, 24);
let ay = 450;
if (testData.axes && myResult.axis_scores) {
ctx.fillStyle = '#64748B';
ctx.font = '14px "PingFang SC", sans-serif';
testData.axes.forEach(a => {
const s = myResult.axis_scores[a.id] || 0;
const maxS = testData.scoring.max_per_axis || 9;
const pct = Math.round(s / maxS * 100);
ctx.fillText(a.emoji_left + ' ' + a.left + ' — ' + pct + '% — ' + a.right + ' ' + a.emoji_right, 300, ay);
ay += 28;
});
}
ctx.fillStyle = '#94A3B8';
ctx.font = '13px "PingFang SC", sans-serif';
ctx.fillText('葫芦数学 · MathBTI', 300, 730);
ctx.save();ctx.globalAlpha=.06;ctx.font='80px sans-serif';ctx.fillStyle='#A8907E';
ctx.fillText('🏮',480,680);
ctx.restore();
ctx.fillText('你也来测测你的数学人格类型 →', 300, 755);
ctx.fillStyle = '#E2E8F0';
ctx.fillRect(255, 640, 90, 90);
ctx.fillStyle = '#94A3B8';
ctx.font = '11px sans-serif';
ctx.fillText('扫码测', 300, 685);
document.getElementById('shareCardPreview').style.display = 'block';
document.getElementById('shareCardPreview').scrollIntoView({ behavior: 'smooth' });
}
function downloadShareCard() {
const canvas = document.getElementById('shareCardCanvas');
const link = document.createElement('a');
link.download = 'mathbti_' + (myResult ? myResult.binary_code : 'result') + '.png';
link.href = canvas.toDataURL('image/png');
link.click();
showToast('📸 图片已保存!');
}
function wrapText(ctx, text, x, y, maxWidth, lineHeight) {
const lines = [];
let current = '';
for (let i = 0; i < text.length; i++) {
const test = current + text[i];
if (ctx.measureText(test).width > maxWidth && current.length > 0) {
lines.push(current);
current = text[i];
} else {
current = test;
}
}
if (current) lines.push(current);
lines.forEach((line, idx) => {
ctx.fillText(line, x, y + idx * lineHeight);
});
}
function retryQuiz() {
answers = [];
currentQ = 0;
myResult = null;
document.getElementById('bioBody').style.maxHeight = '0px';
document.getElementById('bioArrow').textContent = '▾';
document.getElementById('shareCardPreview').style.display = 'none';
switchScreen('introScreen');
}
function switchScreen(id) {
document.querySelectorAll('.screen').forEach(s => s.classList.remove('active'));
document.getElementById(id).classList.add('active');
window.scrollTo({ top: 0, behavior: 'smooth' });
}
let toastTimer = null;
function showToast(msg) {
const el = document.getElementById('toast');
el.textContent = msg;
el.classList.add('show');
clearTimeout(toastTimer);
toastTimer = setTimeout(() => el.classList.remove('show'), 2200);
}
document.addEventListener('DOMContentLoaded', init);