Archived
- 新增完整的我的页面,包含用户资料展示、使用统计和个人信息编辑功能 - 添加收藏和卡册的快捷入口网格 - 简化导航栏,将原分散的个人中心、我的收藏、我的卡册入口合并为单个「我的」标签 - 编写新页面对应的CSS样式并更新静态资源版本号 - 在数学人生页面添加葫芦侠行的快捷跳转入口
1070 lines
91 KiB
JavaScript
1070 lines
91 KiB
JavaScript
// ===== 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('保存成功');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()}
|