This repository has been archived on 2026-08-09. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
Hulumath-Web-Demo/static/js/desktop.min.js
T
2026-08-08 15:49:06 +08:00

643 lines
59 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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(()=>{});
});