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.js
T
2026-08-08 17:40:47 +08:00

1070 lines
91 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.
// ===== State =====
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='',curDim='',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';
// ===== i18n =====
const I18N={zh:{home:'首页',discover:'发现',toolbox:'工具箱',knowledge:'知识卡片',story:'葫芦侠行',simulator:'数学人生',favorites:'我的收藏',profile:'个人中心',mine:'我的',search:'搜索内容...',guest:'游客用户',
heroTitle:'葫芦数学 · 用玩的方式打开数学',heroSub:'数学不是公式,是你理解世界的方式',heroCTA:'开始探索 →',
exploreTitle:'探索数学的万千可能',tryTitle:'试试这些',
mbtiName:'MathBTI 数学人格',mbtiDesc:'12题发现你的数学人格,领养专属数学小人 →',
toolboxName:'数学工具箱',toolboxDesc:'口算竞赛 · 计算器 · 符号查询 · 函数绘图 →',
storyName:'葫芦侠行',storyDesc:'北宋数学武侠 · 8章93节点 · 8种结局 →',
simName:'数学人生',simDesc:'四大主题 · 更多数学人生敬请期待 →',
videoName:'发现视频',videoDesc:'五维能力地图 · 看视频点亮成长',cardName:'知识卡片',cardDesc:'8个数学话题',
aiName:'AI 助教',aiDesc:'随时解答数学疑问',
petTip:'每天使用工具箱和模拟器获取经验升级',
dailyFactPrefix:'💡 数学冷知识:',
discoverGreet:'Hello there. 这里收集了来自各专业志愿者的数学科普视频。',
mapTitle:'五维能力地图',mapSub:'观看视频,点亮数学小人的五种能力',mapHint:'点击地图节点,进入对应维度的视频流',
dimAll:'全部视频',dimVision:'数学眼光',dimHumanity:'数学人文',dimDetective:'数学侦探',dimModeling:'数学建模',dimConnection:'数学联结',
claimVideo:'看完打卡',claimDone:'已学习',claimNeedPet:'先做 MathBTI 领养数学小人,再看视频点亮能力',
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:'四大主题 · 更多数学人生敬请期待',
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:'四大主题 · 更多数学人生敬请期待',
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:'Math Life',favorites:'Favorites',profile:'Profile',mine:'Mine',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:'Four themes. More math lives coming soon. →',
videoName:'Discover Videos',videoDesc:'Five-dimension map · watch and grow',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.',
mapTitle:'Five-Dimension Ability Map',mapSub:'Watch videos and light up your math sprite\'s five abilities',mapHint:'Tap a map node to enter its video stream',
dimAll:'All videos',dimVision:'Math Vision',dimHumanity:'Math Humanity',dimDetective:'Math Detective',dimModeling:'Math Modeling',dimConnection:'Math Connection',
claimVideo:'Finish & earn',claimDone:'Learned',claimNeedPet:'Take MathBTI first to adopt a math sprite, then watch videos to grow it',
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 Life',simSub:'Four themes. More math lives coming soon.',
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 Life',staticSimSub:'Four themes. More math lives coming soon.',
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(()=>{});
}
// ===== Sidebar =====
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')}
// ===== Global Search =====
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')});
// ===== AI Panel =====
function toggleAIPanel(){
var panel=document.getElementById('aiPanel');
if(!panel)return;
var modal=document.getElementById('toolModal');
if(modal)modal.classList.remove('show');
if(panel.classList.contains('open')){
panel.classList.remove('open');
}else{
panel.classList.add('open');
}
}
// ===== API =====
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}
// ===== Route =====
function navigateTo(p){
CP=p;closeSidebar();closeToolModal();document.getElementById('searchResults').classList.remove('show');
// 离开首页时停止 Hero 动画循环,避免空转耗电
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();else if(p==='mine')loadMine();
}
// ===== Home (Desmos-style) =====
let heroAnimId=null;
async function loadHome(){
chatCtx={};
// Hero canvas animation
setTimeout(drawHeroCanvas,100);
// Pet
renderPetCard();
// Feature cards (2x2)
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('');
// Extra cards (1x3)
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('');
// Daily fact
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();
}
// ===== Discover: Five-Dimension Ability Map =====
const DIM5=[
{id:'vision',key:'眼光',label:'数学眼光',short:'眼光',icon:'👁',color:'#5B6AF0',x:50,y:12,desc:'洞察本质的直觉,一眼看穿背后的数学结构。',rep:'庞加莱'},
{id:'humanity',key:'人文',label:'数学人文',short:'人文',icon:'📜',color:'#B06A3B',x:17,y:36,desc:'思想溯源的厚度,理解数学背后的人与史。',rep:'希帕提娅'},
{id:'detective',key:'侦探',label:'数学侦探',short:'侦探',icon:'🔍',color:'#2E7D72',x:24,y:86,desc:'逻辑溯源的严谨,在迷雾中找到唯一真相。',rep:'图灵'},
{id:'modeling',key:'建模',label:'数学建模',short:'建模',icon:'🏗️',color:'#C47F3A',x:76,y:86,desc:'解决现实的力量,把公式变成改变世界的武器。',rep:'冯·诺依曼'},
{id:'connection',key:'联结',label:'数学联结',short:'联结',icon:'🔗',color:'#7B5EA7',x:83,y:36,desc:'跨界融合的通感,连接数学内外的一切。',rep:'牛顿'}
];
const DIM_EXTRA_VIDEOS=[
{id:9101,module:'数人',sub_category:'数学人文',discipline:'数学人文',discipline_icon:'📜',title:'赵爽弦图:《周髀算经》里的人类智慧',video_url:'https://www.bilibili.com/video/BV1example301',description:'从勾股定理的证明看数学如何成为文明的火种。赵爽的弦图,是数学人文的第一张草图。',author:'数学人文馆',duration_min:6},
{id:9102,module:'数人',sub_category:'数学人文',discipline:'数学人文',discipline_icon:'📜',title:'希帕提娅:为理性光芒守护到最后一刻',video_url:'https://www.bilibili.com/video/BV1example302',description:'一位女数学家在动荡年代坚持讲授数学,理解她,就理解了数学背后的信仰。',author:'数学人文馆',duration_min:7},
{id:9103,module:'数人',sub_category:'数学人文',discipline:'数学人文',discipline_icon:'📜',title:'图灵与恩尼格玛:密码背后的人与史',video_url:'https://www.bilibili.com/video/BV1example303',description:'从密码战争到计算机科学,图灵用逻辑改变了历史走向。',author:'数学人文馆',duration_min:8},
{id:9104,module:'数说',sub_category:'几何直觉',discipline:'数学眼光',discipline_icon:'👁',title:'向日葵、贝壳与斐波那契:用数学眼光看自然',video_url:'https://www.bilibili.com/video/BV1example304',description:'当你看一朵花不再只看到美,而是看到数列与结构,数学眼光就亮了。',author:'数学眼光馆',duration_min:5},
{id:9105,module:'数说',sub_category:'几何直觉',discipline:'数学眼光',discipline_icon:'👁',title:'城市建筑里的对称与黄金比例',video_url:'https://www.bilibili.com/video/BV1example305',description:'从古典建筑到现代地标,用数学眼光捕捉城市里隐藏的几何秩序。',author:'数学眼光馆',duration_min:5},
{id:9106,module:'数说',sub_category:'几何直觉',discipline:'数学眼光',discipline_icon:'👁',title:'庞加莱猜想:直觉如何带人类走进更高维度',video_url:'https://www.bilibili.com/video/BV1example306',description:'像庞加莱一样,在踏上公交车的一瞬间,看见隐藏在空间里的结构。',author:'数学眼光馆',duration_min:6}
];
function videoDimKey(v){
const t=(v.title||'')+(v.sub_category||'')+(v.discipline||'');
if(/人文|赵爽|周髀|希帕提娅|数学史|恩尼格玛|战火|信仰|守护/.test(t))return'人文';
if(/地图|画不准|高斯绝妙|斐波那契|向日葵|黄金比例|对称|眼光|直觉|庞加莱/.test(t))return'眼光';
if(/强化学习|AlphaGo|人工智能|神经网络|深度学习|矩阵|GPU/.test(t))return'联结';
if(/密码|网络安全|统计|平均数|彩票|费米|图论|六度|二进制|卡尔曼|PLC|布尔|集成电路|雷达|多普勒|编译|形式语言|B\+树|数据库/.test(t))return'侦探';
if(/金融|风险|经济|市场|供应链|线性规划|物流|回归|建模|数学模型|反应动力学|传质|材料|气候变化|污水处理|质量平衡|种群|扩散|高斯模型|中子|AHP|Matlab|Python|数模|最优控制|机器人控制|自行车|齿轮|振动|有限元/.test(t))return'建模';
if(/傅里叶|麦克斯韦|采样|拉东|心电|DNA|信息论|水平集|渐开线|薛定谔|边界层|熵增|太阳能|空间群|光刻|半导体|计算机视觉|几何变换/.test(t))return'联结';
return'联结';
}
function dimInfo(v){return DIM5.find(d=>d.key===videoDimKey(v))||DIM5[4]}
function mergeAllV(list){
const arr=(list||[]).slice();
DIM_EXTRA_VIDEOS.forEach(e=>{if(!arr.some(v=>v.id===e.id))arr.push(e)});
const dm={};
arr.forEach(v=>{if(v.discipline&&!dm[v.discipline])dm[v.discipline]={discipline:v.discipline,discipline_icon:v.discipline_icon||'·'}});
vD=Object.values(dm);
return arr;
}
async function loadDiscover(){
chatCtx={};
if(favP){await favP;favP=null}
if(!allV.length){
renderSkeletons('discoverVideos',6);
try{
const r=await api('/api/videos');
if(r.code===0)allV=mergeAllV(r.data||[]);
}catch(e){
document.getElementById('discoverVideos').innerHTML='<p style="color:#8A8580;text-align:center;padding:40px;grid-column:1/-1">'+t('loadFailVideo')+'</p>';
allV=mergeAllV([]);
}
}else{
allV=mergeAllV(allV);
}
rDF();rDimTabs();rDMap();rVCNP('discoverVideos',filterVideos());
}
function dimVideoCount(k){return allV.filter(v=>dimInfo(v).key===k).length}
function filterVideos(){return allV.filter(v=>(!curDim||dimInfo(v).key===curDim)&&(!cD||v.discipline===cD))}
function rDMap(){
const el=document.getElementById('dimMap');if(!el)return;
const pet=getPet();
const lines=DIM5.map((d,i)=>{
const n=DIM5[(i+1)%DIM5.length];
return '<line x1="'+d.x+'" y1="'+d.y+'" x2="'+n.x+'" y2="'+n.y+'"/>';
}).join('');
const spokes=DIM5.map(d=>'<line class="dim-map-spoke" x1="50" y1="50" x2="'+d.x+'" y2="'+d.y+'"/>').join('');
el.innerHTML=
'<div class="dim-map-head"><div><div class="dim-map-title">'+t('mapTitle')+'</div><div class="dim-map-sub">'+t('mapSub')+'</div></div><div class="dim-map-legend">'+DIM5.map(d=>'<span class="dim-map-legend-item" style="--dim-color:'+d.color+'"><i></i>'+esc(d.label)+' · '+dimVideoCount(d.key)+'</span>').join('')+'</div></div>'+
'<div class="dim-map-board"><svg class="dim-map-svg" viewBox="0 0 100 100" preserveAspectRatio="none">'+spokes+lines+'</svg>'+
'<div class="dim-map-center"><span class="dim-map-gourd">葫</span><span>数学小人</span></div>'+
DIM5.map(d=>{const v=pet&&pet.dims?Math.min(100,Math.max(0,Number(pet.dims[d.key])||0)):null;return '<button class="dim-map-node'+(curDim===d.key?' active':'')+'" style="left:'+d.x+'%;top:'+d.y+'%;--dim-color:'+d.color+'" title="'+esc(d.desc)+' 代表人物:'+esc(d.rep)+'" onclick="fDim(\''+d.key+'\');scrollToVideos()"><span class="dim-node-icon">'+d.icon+'</span><span class="dim-node-name">'+esc(d.label)+'</span><span class="dim-node-value">'+(v===null?'--':v)+'</span></button>'}).join('')+
'</div>'+
'<div class="dim-map-foot">'+t('mapHint')+'</div>';
}
function rDimTabs(){
const el=document.getElementById('dimTabs');if(!el)return;
let h='<button class="dim-tab'+(curDim===''?' active':'')+'" onclick="fDim(\'\')">'+t('filterAll')+'<span>'+allV.length+'</span></button>';
DIM5.forEach(d=>{h+='<button class="dim-tab'+(curDim===d.key?' active':'')+'" style="--dim-color:'+d.color+'" onclick="fDim(\''+d.key+'\')">'+d.icon+' '+esc(d.label)+'<span>'+dimVideoCount(d.key)+'</span></button>'});
el.innerHTML=h;
}
function fDim(d){curDim=d;rDimTabs();rDMap();rVCNP('discoverVideos',filterVideos())}
function scrollToVideos(){const el=document.getElementById('discoverVideos');if(el)el.scrollIntoView({behavior:'smooth',block:'start'})}
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',filterVideos())}
// ===== 视频卡片网格:无限滚动(每次12条) =====
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||'',dim=dimInfo(v);
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)+'\',\''+esc(dim.key)+'\')"';
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-dim-badge" style="--dim-color:'+dim.color+'">'+dim.icon+' '+esc(dim.label)+'</div><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>';
}
function openVideo(id,title,desc,url,author,disc,dimKey){
chatCtx={type:'video',title:title};rH('video',id,title);
const found=allV.find(v=>v.id===id);
if(!title&&found)title=found.title;
if(!desc&&found)desc=found.description;
const dim=dimKey?DIM5.find(d=>d.key===dimKey):(found?dimInfo(found):dimInfo({title:title,discipline:disc}));
const pet=getPet();
const claims=getVideoClaims();
const claimed=!!claims['video_'+id];
const claimBox=pet
?'<div class="video-claim-box" style="--dim-color:'+dim.color+'">'+(claimed
?'<button class="video-claim-btn claimed" disabled>✅ '+t('claimDone')+' · '+esc(dim.label)+' +3</button>'
:'<button class="video-claim-btn" onclick="claimVideo('+id+',\''+esc(dim.key)+'\',\''+esc(dim.label)+'\')">🎯 '+t('claimVideo')+' · '+esc(dim.label)+' +3</button>')+'</div>'
:'<div class="video-claim-box"><div class="video-claim-hint">'+t('claimNeedPet')+'</div></div>';
let h='<div class="video-modal-top"><span class="video-modal-dim" style="--dim-color:'+dim.color+'">'+dim.icon+' '+esc(dim.label)+'</span><h2 class="video-modal-title">'+esc(title)+'</h2></div>';
h+=url?'<div class="video-modal-player"><iframe src="'+esc(url)+'" title="'+esc(title)+'" allowfullscreen></iframe></div>':'<div class="video-modal-player empty"><span>▶</span></div>';
h+='<p class="video-modal-desc">'+esc(desc||'No description')+'</p><div class="video-modal-meta"><span>'+esc(author)+'</span><span>'+esc(disc)+'</span><span>'+esc(dim.label)+'</span></div>';
h+=claimBox+'<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');
}
function getVideoClaims(){try{return JSON.parse(localStorage.getItem('mb_video_claims')||'{}')||{}}catch(e){return{}}}
function claimVideo(id,dimKey,label){
if(!getPet())return;
const claims=getVideoClaims();
if(claims['video_'+id])return;
claims['video_'+id]=true;
localStorage.setItem('mb_video_claims',JSON.stringify(claims));
addPetDim(dimKey,3);
addPetExp(10);
const btn=document.querySelector('.video-claim-btn');
if(btn){btn.disabled=true;btn.classList.add('claimed');btn.innerHTML='✅ '+t('claimDone')+' · '+esc(label)+' +3'}
showToast(esc(label)+' +3 · 数学小人成长中');
rDMap();
}
// ===== Knowledge Cards (standalone page) =====
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)}
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')}
// ===== Toolbox =====
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()}
// ===== Story =====
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}
// ===== Simulator =====
async function loadSimulator(){loadMathLife()}
async function loadLegacySimulator(){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}};loadLegacySimulator()}
// ===== Favorites =====
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()}
// ===== History =====
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){}}
// ===== Profile =====
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('保存成功');if(CP==='mine')loadMine();else loadProfile()}catch(e){alert('保存失败')}}
// ===== Card Collection Page =====
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')+'">'+portraitHTML(info,24)+'</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>'+stats5HTML(info)
:'<div class="cc-card-icon locked-icon">'+portraitHTML(info,24)+'</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;
}
// ===== Mine Page (我的 — 个人中心 + 收藏 + 卡册入口) =====
async function loadMine(){
chatCtx={};
const c=document.getElementById('mineContent');
if(!c)return;
let p={nickname:'游客用户',avatar_emoji:'葫',avatar_color:'#5B6AF0'};
let s={watch_count:0,favorite_count:0};
try{
const[pr,sr]=await Promise.all([api('/api/user/profile?user_id='+USER_ID),api('/api/user/stats?user_id='+USER_ID)]);
if(pr.code===0&&pr.data)p=pr.data;
if(sr.code===0&&sr.data)s=sr.data;
}catch(e){}
c.innerHTML=
'<div class="page-header"><h1 class="page-title">👤 我的</h1></div>'+
'<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);margin-bottom:20px"><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>'+
'<div class="mine-grid">'+
'<div class="mine-entry" onclick="navigateTo(\'favorites\')"><div class="mine-entry-icon">❤️</div><div class="mine-entry-body"><div class="mine-entry-name">我的收藏</div><div class="mine-entry-desc">已收藏的视频和知识卡片</div></div><span class="mine-entry-arrow">→</span></div>'+
'<div class="mine-entry" onclick="navigateTo(\'cards\')"><div class="mine-entry-icon">🎴</div><div class="mine-entry-body"><div class="mine-entry-name">我的卡册</div><div class="mine-entry-desc">收集全部16位数学人物 · '+cardState.owned.length+'/16</div></div><span class="mine-entry-arrow">→</span></div>'+
'</div>';
}
// ===== Pet Experience =====
function getPet(){
let pet=null;
try{
const a=localStorage.getItem('mb_pet');
if(a)pet=JSON.parse(a);
if(!pet||!pet.type){
const b=localStorage.getItem('mathbrain_pet');
if(b)pet=JSON.parse(b);
}
}catch(e){}
if(!pet||!pet.type)return null;
pet.name=pet.name||pet.type;
pet.level=pet.level||1;
pet.exp=pet.exp||0;
pet.dims=pet.dims||{眼光:10,人文:10,侦探:10,建模:10,联结:10};
return pet;
}
function savePet(pet){if(!pet)return;localStorage.setItem('mb_pet',JSON.stringify(pet));localStorage.setItem('mathbrain_pet',JSON.stringify(pet))}
function renderPetCard(){
const pet=getPet(),pa=document.getElementById('homePetArea');
if(!pa)return;
if(!pet){pa.innerHTML='';return}
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-dims">'+DIM5.map(d=>{const v=Math.min(100,Math.max(0,Number((pet.dims||{})[d.key])||0));return '<div class="pet-dim" title="'+esc(d.label)+' '+v+'"><span style="color:'+d.color+'">'+d.icon+'</span><span class="pet-dim-name">'+esc(d.short)+'</span><div class="pet-dim-track"><i style="width:'+v+'%;background:'+d.color+'"></i></div><b>'+v+'</b></div>'}).join('')+'</div><div class="pet-level">'+t('petTip')+'</div></div></div>';
}
function addPetDim(dimKey,amt){
const pet=getPet();if(!pet)return false;
pet.dims=pet.dims||{眼光:10,人文:10,侦探:10,建模:10,联结:10};
pet.dims[dimKey]=Math.min(100,Math.max(0,(Number(pet.dims[dimKey])||0)+amt));
savePet(pet);
renderPetCard();
return true;
}
function addPetExp(amt){
const pet=getPet();
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}
savePet(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'))}
}
// ===== AI Chat =====
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()}
// ===== Utils =====
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')}
// ===== Float Hub =====
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')});
// ===== Card System =====
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=[];
// Daily login
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;
}
const STAT5_LABELS=['眼光','人文','侦探','建模','联结'];
function stats5HTML(info){
const s=(info&&info.stats5)||{};
return '<div class="stats5">'+STAT5_LABELS.map(k=>{
const v=Math.min(100,Math.max(0,Number(s[k])||0));
return '<div class="stats5-row"><span>'+k+'</span><div class="stats5-track"><i style="width:'+v+'%"></i></div><b>'+(s[k]||0)+'</b></div>';
}).join('')+'</div>';
}
function portraitHTML(info,size){
if(!info)return'?';
const fs=size||24;
if(info.portrait)return '<img src="'+esc(info.portrait)+'" alt="" onerror="this.style.display=\'none\';this.nextElementSibling.style.display=\'flex\'"><span style="display:none;font-size:'+fs+'px">'+(info.icon||'?')+'</span>';
return info.icon||'?';
}
// Render card gallery on home page
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){
// 还没测过 MathBTI
if(!getPet()){
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')+'">'+portraitHTML(info,24)+'</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';
}
}
}
// Gacha draw
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();
// Pity: every 5th draw guaranteed new
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();
// Show result
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>'+
(info?stats5HTML(info):'')+
(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;
});
}
// Card detail modal
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">五维能力</div>'+stats5HTML(info)+'</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)}
}
// ===== Toast =====
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>')}
}
// ===== Init =====
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(()=>{});
});
// ===== Math Life =====
let mtD=null,mtS={currentNode:null,stats:{shuli:10,xiayi:10,xinzhi:10},fav:{}};
const MT_STAGE_MAP={c1:'第一幕 · 大一上:小镇少年走进数学系',c2:'第二幕 · 大一下:第一次完整证明',c3:'第三幕 · 大二上:讨论班与诺特',c4:'第四幕 · 大二下:数学与生活',c5:'第五幕 · 大三上:方向与高斯',c6:'第六幕 · 大三下:科研低谷与诺特',c7:'第七幕 · 大四上:直博申请',c8:'第八幕 · 毕业前夜与直博第一年'};
let alumniList=[],alumniStory=null,alumniStoryIndex=0;
function mtStage(id){const k=(id||'').slice(0,2);return MT_STAGE_MAP[k]||'数学少年线'}
function mtStatColor(k){if(k==='shuli')return '#3F6C8F';if(k==='xinzhi')return '#2E7D72';return '#B8893A'}
function mtStatLabel(k){if(k==='shuli')return '数理根基';if(k==='xinzhi')return '心志韧性';return '同行温度'}
function mtStatIcon(k){if(k==='shuli')return '∑';if(k==='xinzhi')return 'θ';return 'π'}
function mtEffectsHint(ef){if(!ef)return '';const p=[];if(ef.shuli)p.push('数理根基 '+(ef.shuli>0?'+':'')+ef.shuli);if(ef.xiayi)p.push('同行温度 '+(ef.xiayi>0?'+':'')+ef.xiayi);if(ef.xinzhi)p.push('心志韧性 '+(ef.xinzhi>0?'+':'')+ef.xinzhi);if(ef.favorability){for(const k in ef.favorability)p.push(k+'好感 '+(ef.favorability[k]>0?'+':'')+ef.favorability[k])}return p.length?'<span style="font-size:11px;color:#8B5CF6;font-weight:500;display:block;margin-top:4px">['+p.join(' | ')+']</span>':''}
function loadMathLife(){
chatCtx={};
const hub=document.getElementById('mathLifeHub'),teen=document.getElementById('mathTeenPlayer'),legacy=document.getElementById('legacySimPlayer'),alumni=document.getElementById('mathAlumniPlayer');
if(hub)hub.style.display='';
if(teen)teen.style.display='none';
if(legacy)legacy.style.display='none';
if(alumni)alumni.style.display='none';
const themes=[
{icon:'🔱',name:'信仰者',desc:'数学少年线 · 高斯 / 祖冲之 / 欧拉 / 诺特',open:'openMathTeen()',ready:true},
{icon:'📣',name:'传播者',desc:'斐波那契 / 华罗庚 / 伽罗瓦 / Ada Lovelace',open:'',ready:false},
{icon:'🛠️',name:'应用者',desc:'秦九韶 / 牛顿 / 图灵 / 冯·诺依曼',open:'',ready:false},
{icon:'👁️',name:'直觉者',desc:'希帕提娅 / 赵爽 / 庞加莱 / 凯瑟琳·约翰逊',open:'',ready:false}
];
hub.innerHTML=
'<div class="ds-section-title">四大主题</div>'+
'<div class="ds-card-grid ml-theme-grid">'+
themes.map(t=>'<div class="ds-feature-card ml-theme-card'+(t.ready?'':' ml-theme-coming')+'"'+(t.ready?' onclick="'+t.open+'"':'')+'>'+
'<div class="ds-feature-card-icon">'+t.icon+'</div>'+
'<div class="ds-feature-card-name">'+esc(t.name)+'</div>'+
'<div class="ds-feature-card-desc">'+esc(t.desc)+'</div>'+
(t.ready?'':'<div class="ml-coming-badge">敬请期待</div>')+
'</div>').join('')+
'</div>'+
'<button class="ml-alumni-entry ml-story-entry" onclick="navigateTo(\'story\')">'+
'<span class="ml-alumni-entry-icon">⚔️</span>'+
'<span class="ml-alumni-entry-body"><b>葫芦侠行</b><small>北宋数学武侠互动文字冒险 · 8章93节点 · 8+种结局</small></span>'+
'<span class="ml-alumni-entry-arrow">→</span>'+
'</button>'+
'<button class="ml-alumni-entry" onclick="openMathAlumni()">'+
'<span class="ml-alumni-entry-icon">🎓</span>'+
'<span class="ml-alumni-entry-body"><b>数学校友访谈</b><small>真实校友的大学四年 · Skill 剧本包</small></span>'+
'<span class="ml-alumni-entry-arrow">→</span>'+
'</button>'+
'<div class="ml-more">'+
'<div class="ml-more-title">更多数学人生 · 敬请期待</div>'+
'<div class="ml-more-sub">更多主题正在创作中,敬请期待</div>'+
'<div class="ml-more-grid">'+
[
{icon:'📣',name:'传播者',desc:'斐波那契 / 华罗庚 / 伽罗瓦 / Ada Lovelace'},
{icon:'🛠️',name:'应用者',desc:'秦九韶 / 牛顿 / 图灵 / 冯·诺依曼'},
{icon:'👁️',name:'直觉者',desc:'希帕提娅 / 赵爽 / 庞加莱 / 凯瑟琳·约翰逊'}
].map(m=>'<div class="ml-more-card"><div class="ml-more-icon">'+m.icon+'</div><div class="ml-more-name">'+esc(m.name)+'</div><div class="ml-more-desc">'+esc(m.desc)+'</div><div class="ml-more-tag">敬请期待</div></div>').join('')+
'</div>'+
'</div>';
}
async function openMathTeen(){
const hub=document.getElementById('mathLifeHub'),teen=document.getElementById('mathTeenPlayer'),legacy=document.getElementById('legacySimPlayer');
if(hub)hub.style.display='none';
if(legacy)legacy.style.display='none';
if(teen){teen.style.display='';teen.innerHTML='<div class="ml-player-header"><button class="ml-back-btn" onclick="backToMathLife()">← 返回数学人生</button><div class="ml-player-title">🧑‍🎓 数学少年线</div><div class="ml-player-sub" id="mtStage">正在加载剧本…</div></div><div class="sim-container"><div class="ml-stats" id="mtStats"></div><div id="mtStoryBox"><div class="spinner"></div></div><div id="mtChoices" class="story-choices"></div></div>'}
try{const r=await api('/api/story/math_teen');if(r.code===0){mtD=r.data;const saved=localStorage.getItem('mb_math_teen_save');if(saved){try{mtS=JSON.parse(saved)}catch(e){mtS={currentNode:null,stats:{shuli:10,xiayi:10,xinzhi:10},fav:{}}}}const sn=mtD.start_node||'c1_notice';const ni=(mtS.currentNode&&mtD.nodes&&mtD.nodes[mtS.currentNode])?mtS.currentNode:sn;mtS.currentNode=ni;rMTNode(ni)}else throw new Error('load failed')}catch(e){const b=document.getElementById('mtStoryBox');if(b)b.innerHTML='<p style="color:#EF4444">数学少年线加载失败</p>'}}
function rMTNode(ni){
if(!mtD||!mtD.nodes||!mtD.nodes[ni]){document.getElementById('mtStoryBox').innerHTML='<p style="color:#EF4444">节点未找到</p>';return}
const n=mtD.nodes[ni];mtS.currentNode=ni;localStorage.setItem('mb_math_teen_save',JSON.stringify(mtS));
const st=document.getElementById('mtStage');if(st)st.textContent=mtStage(ni);
const stats=document.getElementById('mtStats');if(stats)stats.innerHTML=
['shuli','xiayi','xinzhi'].map(k=>'<div class="ml-stat"><div class="ml-stat-icon" style="background:'+mtStatColor(k)+'">'+mtStatIcon(k)+'</div><b>'+(mtS.stats[k]||0)+'</b><span>'+mtStatLabel(k)+'</span></div>').join('')+
'<div class="ml-stat"><div class="ml-stat-icon" style="background:#A64A44">4</div><b>4 位数学家</b><span>高斯 · 祖冲之 · 欧拉 · 诺特</span></div>'+
'<button onclick="resetMathTeen()" style="margin-left:auto;padding:6px 14px;border-radius:8px;border:1px solid #D6D1CA;background:#FDFCFA;cursor:pointer;font-size:12px;color:#8A8580">🔄 重新开始</button>';
const box=document.getElementById('mtStoryBox');if(box)box.innerHTML=(n.character?'<div class="story-character">'+esc(n.character)+'</div>':'')+'<div class="story-text">'+esc(n.scene||'')+'</div>';
rMTChoices(n.choices||[]);
}
function rMTChoices(choices){
const area=document.getElementById('mtChoices');if(!area)return;
if(!choices.length){area.innerHTML='<div class="end-note">故事结束 · 你的数学,刚刚开始</div><button class="story-choice" id="mtRestartBtn">再走一次这段路</button>';const rb=document.getElementById('mtRestartBtn');if(rb)rb.addEventListener('click',resetMathTeen);return}
area.innerHTML=choices.map((c,i)=>'<button class="story-choice" data-mt-index="'+i+'">'+esc(c.text)+mtEffectsHint(c.effects)+'</button>').join('');
area.querySelectorAll('.story-choice').forEach(btn=>{
const c=choices[Number(btn.getAttribute('data-mt-index'))];
btn.addEventListener('click',()=>mTC(c.next,c.effects));
});
}
function mTC(next,ef){if(ef){if(ef.shuli)mtS.stats.shuli=(mtS.stats.shuli||10)+ef.shuli;if(ef.xiayi)mtS.stats.xiayi=(mtS.stats.xiayi||10)+ef.xiayi;if(ef.xinzhi)mtS.stats.xinzhi=(mtS.stats.xinzhi||10)+ef.xinzhi;if(ef.favorability){if(!mtS.fav)mtS.fav={};for(const k in ef.favorability)mtS.fav[k]=(mtS.fav[k]||0)+ef.favorability[k]}}rMTNode(next);addPetExp(5)}
function resetMathTeen(){localStorage.removeItem('mb_math_teen_save');mtS={currentNode:null,stats:{shuli:10,xiayi:10,xinzhi:10},fav:{}};if(mtD&&mtD.start_node)rMTNode(mtD.start_node);else openMathTeen()}
function backToMathLife(){loadMathLife()}
async function openMathAlumni(){
const hub=document.getElementById('mathLifeHub'),teen=document.getElementById('mathTeenPlayer'),legacy=document.getElementById('legacySimPlayer'),alumni=document.getElementById('mathAlumniPlayer');
if(hub)hub.style.display='none';
if(teen)teen.style.display='none';
if(legacy)legacy.style.display='none';
if(!alumni)return;
alumni.style.display='';
alumni.innerHTML='<div class="ml-player-header"><button class="ml-back-btn" onclick="backToMathLife()">← 返回数学人生</button><div class="ml-player-title">数学校友访谈</div><div class="ml-player-sub">真实校友的数学人生 · Skill 剧本包</div></div><div id="alumniListView"><div class="ml-alumni-list-head"><div class="ml-alumni-title">校友卡片</div><div class="ml-alumni-sub">点进一位校友,线性体验他/她的大学四年</div></div><div id="alumniCards"><div class="spinner"></div></div></div><div id="alumniStoryView" style="display:none"></div>';
loadAlumniCards();
}
async function loadAlumniCards(){
try{const r=await api('/api/alumni');alumniList=(r&&r.code===0&&Array.isArray(r.data))?r.data:[]}catch(e){alumniList=[]}
const box=document.getElementById('alumniCards');if(!box)return;
const cards=alumniList.length
?alumniList.map(alumniCardHTML).join('')
:[1,2,3,4,5,6].map(i=>'<button class="ml-alumni-card placeholder" onclick="openAlumniStory(\'PLACEHOLDER-'+i+'\')"><div class="ml-alumni-avatar">🎓</div><div class="ml-alumni-code">AL-'+(String(i).padStart(3,'0'))+'</div><div class="ml-alumni-name">校友档案 '+i+'</div><div class="ml-alumni-meta">大一 → 大四 · 访谈整理中</div><span class="ml-alumni-skill-tag">Skill 剧本待产出</span></button>').join('');
box.innerHTML=cards+'<button class="ml-alumni-slot join" onclick="showToast(\'校友访谈计划筹备中,欢迎提供校友线索\')"><div class="ml-alumni-avatar"></div><div class="ml-alumni-name">成为校友访谈嘉宾</div><div class="ml-alumni-meta">报名与访谈流程即将开放</div></button>';
}
function alumniCardHTML(a){
const skill=a.skill||{},tags=(skill.tags&&skill.tags.length)?skill.tags:['数学人生'];
return '<button class="ml-alumni-card" onclick="openAlumniStory(\''+esc(a.id)+'\')">'+
'<div class="ml-alumni-avatar">'+esc(a.icon||'🎓')+'</div>'+
'<div class="ml-alumni-code">'+esc(a.id||'AL-000')+'</div>'+
'<div class="ml-alumni-name">'+esc(a.name||'未命名校友')+'</div>'+
'<div class="ml-alumni-meta">'+esc((a.major||'数学')+' · '+(a.graduation_year||'已毕业'))+'</div>'+
'<div class="ml-alumni-skill-tags">'+tags.map(tg=>'<span class="ml-alumni-skill-tag">'+esc(tg)+'</span>').join('')+'</div>'+
'<div class="ml-alumni-dim-row">'+DIM5.map(d=>{const v=(a.dims&&a.dims[d.key])||0;return '<span title="'+esc(d.label)+'">'+d.icon+' '+v+'</span>'}).join('')+'</div>'+
'<span class="ml-alumni-card-cta">线性体验四年 →</span>'+
'</button>';
}
function openAlumniStory(id){
alumniStory=alumniList.find(a=>a.id===id)||null;
alumniStoryIndex=0;
const list=document.getElementById('alumniListView'),story=document.getElementById('alumniStoryView');
if(list)list.style.display='none';
if(story){
story.style.display='';
story.innerHTML='<div class="ml-player-header"><button class="ml-back-btn" onclick="backToAlumniList()">← 返回校友卡片</button><div class="ml-player-title">'+esc(alumniStory?alumniStory.name:'校友剧本')+'</div><div class="ml-player-sub">'+esc(alumniStory?(alumniStory.career||'大学四年的数学人生'):'剧本整理中')+'</div></div><div id="alumniStoryBody"></div>';
renderAlumniStory();
}
}
function backToAlumniList(){
const list=document.getElementById('alumniListView'),story=document.getElementById('alumniStoryView');
if(list)list.style.display='';
if(story)story.style.display='none';
}
function renderAlumniStory(){
const body=document.getElementById('alumniStoryBody');if(!body)return;
const nodes=alumniStory&&alumniStory.four_years&&alumniStory.four_years.nodes?alumniStory.four_years.nodes:[];
if(!nodes.length){
body.innerHTML='<div class="ml-story-linear"><div class="ml-alumni-empty">该校友的 Skill 剧本正在整理中。</div><div class="ml-story-timeline">'+['大一','大二','大三','大四'].map((s,i)=>'<div class="ml-story-stage"><span>'+(i+1)+'</span><b>'+s+'</b><small>剧情节点整理中</small></div>').join('')+'</div></div>';
return;
}
const n=nodes[Math.min(alumniStoryIndex,nodes.length-1)];
const pct=Math.round((alumniStoryIndex+1)/nodes.length*100);
body.innerHTML='<div class="ml-story-linear"><div class="ml-story-progress"><i style="width:'+pct+'%"></i></div><div class="ml-story-stage-name">'+esc(n.stage||('第 '+(alumniStoryIndex+1)+' 幕'))+'</div><div class="ml-story-scene">'+esc(n.scene||'')+'</div>'+(n.effect?'<div class="ml-story-effect">'+fiveDimEffectHTML(n.effect)+'</div>':'')+
(alumniStoryIndex<nodes.length-1
?'<button class="ml-story-next" onclick="nextAlumniNode()">下一幕 →</button>'
:'<div class="ml-story-ending">'+esc(n.ending||'这就是这位校友的数学人生。')+'</div><button class="ml-story-next" onclick="backToAlumniList()">返回校友卡片</button>')+
'</div>';
}
function nextAlumniNode(){alumniStoryIndex++;renderAlumniStory()}
function fiveDimEffectHTML(ef){
if(!ef)return'';
const labels={眼光:'数学眼光',人文:'数学人文',侦探:'数学侦探',建模:'数学建模',联结:'数学联结'};
const arr=Object.keys(ef||{}).filter(k=>labels[k]&&ef[k]);
if(!arr.length)return'';
return '<span>五维能力关联:'+arr.map(k=>labels[k]+' '+(ef[k]>0?'+':'')+ef[k]).join(' · ')+'</span>';
}
function openLegacySim(){const hub=document.getElementById('mathLifeHub'),teen=document.getElementById('mathTeenPlayer'),legacy=document.getElementById('legacySimPlayer');if(hub)hub.style.display='none';if(teen)teen.style.display='none';if(legacy)legacy.style.display='';loadLegacySimulator()}