/* ========================================================= app.js — 前端交互逻辑 v7.0 · 动态知识卡片 + 收藏历史 + 个人中心 ========================================================= */ // ---- 全局状态 ---- let currentModule = "发现"; let currentVideoUrl = ""; let currentVideoTitle = ""; let currentDiscipline = "all"; let currentSubTab = "recommend"; let currentLifeTab = "story"; let allVideos = []; let allKnowledgeCards = []; let storyData = null; let simulatorData = null; let currentContext = { type: "", title: "" }; let userId = localStorage.getItem("mathAppUserId") || generateUserId(); let userFavorites = { video: {}, knowledge: {} }; let userAvatar = localStorage.getItem("mathAppAvatar") || "葫"; let userAvatarColor = localStorage.getItem("mathAppAvatarColor") || "#A0B9D8"; let vnState = { currentNode: null, stats: { shuli: 10, xiayi: 10, xinzhi: 10 }, favorability: { "欧阳侠": 0, "Eddie侠": 0, "厚朴侠": 0, "三力思侠": 0, "溜溜侠": 0, "孔锝侠": 0, "星侠": 0, "甲亢侠": 0 } }; let simState = { currentNode: null, stats: { gpa: 10, interest: 10, skill: 10 }, history: [] }; const SIM_STAT_LABELS = { gpa: "绩点", interest: "兴趣", skill: "技能" }; const SIM_STAT_COLORS = { gpa: "#A0B9D8", interest: "#DAB8B0", skill: "#B8D0C9" }; const SIM_STAT_EMOJIS = { gpa: "📊", interest: "❤️", skill: "🔧" }; function generateUserId() { var id = "user_" + Date.now().toString(36) + "_" + Math.random().toString(36).substr(2, 6); localStorage.setItem("mathAppUserId", id); return id; } const STAT_LABELS = { shuli: "数理根基", xiayi: "侠义声望", xinzhi: "心志韧性" }; const STAT_COLORS = { shuli: "#A0B9D8", xiayi: "#DAB8B0", xinzhi: "#B8D0C9" }; const STAT_EMOJIS = { shuli: "📐", xiayi: "⚔️", xinzhi: "🧘" }; // 章节检测:根据节点ID前缀判断当前章节 function detectChapter(nodeId) { if (!nodeId) return 1; var m = nodeId.match(/^ch(\d+)/); return m ? parseInt(m[1]) : 1; } // ---- 模块配色 ---- const MODULE_COLORS = { "发现": { color: "#A0B9D8", gradient: "linear-gradient(135deg, #A0B9D8, #8AA3C2)", lightBg: "#DCE6F0", name: "发现 · 值得一看的数学内容" }, "知识": { color: "#B8D0C9", gradient: "linear-gradient(135deg, #B8D0C9, #A0BBAF)", lightBg: "#E4EFEB", name: "知识 · 数学发现" }, "数学人生": { color: "#DAB8B0", gradient: "linear-gradient(135deg, #DAB8B0, #C9A39A)", lightBg: "#F2E4DF", name: "数学人生 · 大学选择模拟" }, "我的": { color: "#C5CAD0", gradient: "linear-gradient(135deg, #C5CAD0, #AEB3B9)", lightBg: "#EDEEF0", name: "我的 · 个人中心" } }; const MODULE_BANNERS = { "发现": { icon: "◎", title: "发现值得一看的数学内容", sub: "从 12 个专业分区进入,直接查看该方向的视频内容" }, "知识": { icon: "≡", title: "用数学视角理解世界", sub: "把复杂概念变成轻量、好懂、适合反复刷的知识卡片" }, "数学人生": { icon: "◇", title: "葫芦侠行 · 互动剧情", sub: "八章完整剧情 | 四属性系统 | 多结局 | 北宋数学武侠世界" }, "我的": { icon: "✦", title: "你的学习空间", sub: "查看资料、收藏、记录和剧情存档" } }; const MODULE_CSS_CLASS = { "发现": "shuren", "知识": "shushuo", "数学人生": "shusi", "我的": "shuwu" }; const DISCIPLINE_ICONS = { "计算机": { icon: "💻", symbol: "⌨", color: "#fff", bg: "#8BA4C0" }, "机械工程": { icon: "⚙️", symbol: "⚙", color: "#fff", bg: "#B8A99A" }, "电子信息": { icon: "📡", symbol: "⚡", color: "#fff", bg: "#9BB8AE" }, "经济管理": { icon: "📊", symbol: "📈", color: "#fff", bg: "#C5A8A0" }, "生物医学": { icon: "🧬", symbol: "❤", color: "#fff", bg: "#C4B0B8" }, "化工材料": { icon: "🧪", symbol: "⚗", color: "#fff", bg: "#ACAEC5" }, "人工智能": { icon: "🤖", symbol: "AI", color: "#fff", bg: "#95ABB8" }, "微电子": { icon: "🔌", symbol: "μ", color: "#fff", bg: "#8BA4C0" }, "自动化": { icon: "🦾", symbol: "⚙", color: "#fff", bg: "#BCA898" }, "能源动力": { icon: "🔥", symbol: "⚡", color: "#fff", bg: "#A3B4A5" }, "环境工程": { icon: "🌱", symbol: "✿", color: "#fff", bg: "#A8B8A8" }, "数学建模": { icon: "📐", symbol: "Σ", color: "#fff", bg: "#9CAEC4" }, "通用": { icon: "📚", symbol: "◎", color: "#fff", bg: "#ADB5BD" } }; const AVATAR_COLORS = ["#A0B9D8","#B8D0C9","#DAB8B0","#C5CAD0","#B8A99A","#9BB8AE","#C4B0B8","#ACAEC5"]; // ---- 剧本广场定义 ---- const SCRIPTS = [ { id: "huluxiaxing", name: "葫芦侠行", icon: "📖", desc: "北宋年间,紫金山下秉公习武堂。你扮演葫芦侠,用数学武学闯荡江湖。", meta: "93节点 · 8章 · 8+结局", status: "ready", cssClass: "ss-card-wuxia", engine: "story" }, { id: "simulator", name: "数学专业模拟器", icon: "🎓", desc: "跑完四年大学数学专业的全流程。选课、考试、分流、毕业——你的每个选择决定结局。", meta: "8学期 · 大三专业分流 · 多结局", status: "beta", cssClass: "ss-card-simulator", engine: "simulator" }, { id: "mathbti", name: "数学人格测试", icon: "🧮", desc: "12道场景题,发现你的数学人格类型——你是「梦境探索者」还是「博弈游侠」?", meta: "12题 · 4轴测评 · 12人格", status: "ready", cssClass: "ss-card-mathbti", engine: "mathbti" } ]; const COMING_SOON = [ { name: "数学侦探录", icon: "🔍", desc: "民国上海,用数学破案", color: "#B8A99A" }, { name: "星际数探", icon: "🔭", desc: "22世纪太空舰队首席数理官", color: "#9BB8AE" }, { name: "算学千年", icon: "📜", desc: "穿越古代,亲历数学史", color: "#ACAEC5" }, { name: "数学创业记", icon: "🚀", desc: "硅谷数学PhD辍学创业", color: "#C4B0B8" } ]; // ---- 角色立绘映射 (P0-2) ---- const SPRITE_MAP = { "葫芦侠": "https://coresg-normal.trae.ai/api/ide/v1/text_to_image?prompt=young%20Chinese%20Song%20dynasty%20scholar%20with%20a%20gourd%20hanging%20at%20his%20waist%2C%20simple%20blue-gray%20robes%2C%20gentle%20expression%2C%20clean%20ink%20wash%20painting%20style%20on%20soft%20background%2C%20morandi%20muted%20colors&image_size=portrait_4_3", "欧阳侠": "https://coresg-normal.trae.ai/api/ide/v1/text_to_image?prompt=diligent%20young%20Chinese%20scholar%20reading%20a%20book%20at%20dawn%2C%20warm%20candle%20light%2C%20ink%20wash%20style%2C%20wearing%20simple%20robes%2C%20soft%20morandi%20beige%20and%20blue%20tones&image_size=portrait_4_3", "溜溜侠": "https://coresg-normal.trae.ai/api/ide/v1/text_to_image?prompt=mysterious%20young%20Chinese%20female%20warrior%20with%20a%20large%20black%20dog%2C%20veiled%20face%2C%20dark%20grey%20robes%2C%20ink%20wash%20painting%20style%2C%20morandi%20muted%20dark%20tones%2C%20cool%20atmosphere&image_size=portrait_4_3", "孔锝侠": "https://coresg-normal.trae.ai/api/ide/v1/text_to_image?prompt=strict%20Chinese%20martial%20arts%20instructor%2C%20tall%20and%20upright%2C%20stern%20but%20kind%20eyes%2C%20ink%20wash%20style%2C%20wearing%20neat%20grey-blue%20robes%2C%20morandi%20muted%20tones&image_size=portrait_4_3", "Eddie侠": "https://coresg-normal.trae.ai/api/ide/v1/text_to_image?prompt=young%20Chinese%20scholar%20with%20copper%20coin%20spinning%20on%20finger%2C%20playful%20expression%2C%20ink%20wash%20style%2C%20soft%20morandi%20tones&image_size=portrait_4_3", "厚朴侠": "https://coresg-normal.trae.ai/api/ide/v1/text_to_image?prompt=elegant%20Chinese%20theater%20actor%2C%20dramatic%20pose%2C%20traditional%20costume%2C%20ink%20wash%20style%2C%20morandi%20muted%20warm%20tones&image_size=portrait_4_3", "星侠": "https://coresg-normal.trae.ai/api/ide/v1/text_to_image?prompt=gentle%20Chinese%20female%20scholar%20with%20warm%20smile%2C%20ink%20wash%20style%2C%20flowing%20robes%2C%20morandi%20soft%20pink%20and%20grey%20tones&image_size=portrait_4_3", "甲亢侠": "https://coresg-normal.trae.ai/api/ide/v1/text_to_image?prompt=passionate%20young%20Chinese%20debater%2C%20animated%20gestures%2C%20ink%20wash%20style%2C%20morandi%20muted%20tones%2C%20intense%20eyes&image_size=portrait_4_3" }; // ---- 场景背景映射 (P0-2) ---- const BG_MAP = { "紫金山": "https://coresg-normal.trae.ai/api/ide/v1/text_to_image?prompt=ancient%20Chinese%20academy%20on%20misty%20Purple%20Mountain%2C%20traditional%20architecture%20with%20courtyards%2C%20morning%20fog%2C%20ink%20wash%20painting%20style%2C%20morandi%20muted%20greens%20and%20greys&image_size=landscape_4_3", "石室": "https://coresg-normal.trae.ai/api/ide/v1/text_to_image?prompt=underground%20stone%20chamber%20turned%20study%20room%2C%20candle%20lit%2C%20wooden%20shelves%20with%20scrolls%2C%20murals%20on%20rock%20walls%2C%20warm%20atmosphere%2C%20ink%20wash%20style%2C%20morandi%20warm%20greys&image_size=landscape_4_3", "姑苏": "https://coresg-normal.trae.ai/api/ide/v1/text_to_image?prompt=Suzhou%20ancient%20canal%20town%20in%20spring%2C%20stone%20bridges%2C%20willow%20trees%2C%20traditional%20white%20wall%20buildings%2C%20gentle%20rain%2C%20ink%20wash%20painting%20style%2C%20morandi%20muted%20blue-grey%20tones&image_size=landscape_4_3", "京城": "https://coresg-normal.trae.ai/api/ide/v1/text_to_image?prompt=ancient%20Chinese%20capital%20city%2C%20grand%20government%20hall%2C%20solemn%20atmosphere%2C%20ink%20wash%20style%2C%20morandi%20muted%20grey%20tones&image_size=landscape_4_3" }; // 章节 → 默认背景映射 const CHAPTER_BG = { 1: "紫金山", 2: "紫金山", 3: "紫金山", 4: "石室", 5: "紫金山", 6: "石室", 7: "石室", 8: "京城" }; // ---- 页面初始化 ---- document.addEventListener("DOMContentLoaded", async function () { await loadStoryData(); await loadUserFavorites(); loadVNSave(); switchModule("发现"); }); // ============================================================ // 模块切换 // ============================================================ function switchModule(module) { currentModule = module; currentDiscipline = "all"; currentSubTab = "recommend"; var colors = MODULE_COLORS[module] || MODULE_COLORS["发现"]; document.documentElement.style.setProperty("--accent", colors.color); document.documentElement.style.setProperty("--accent-light", colors.lightBg); document.documentElement.style.setProperty("--accent-gradient", colors.gradient); // 顶部品牌栏头像同步 var brandIcon = document.querySelector(".brand-icon-sm"); if (brandIcon) { brandIcon.textContent = "葫"; brandIcon.style.background = userAvatarColor; } // TabBar 高亮 document.querySelectorAll(".tab-btn").forEach(function (btn) { btn.classList.toggle("active", btn.dataset.module === module); }); // 先隐藏所有特殊面板 hideAllPanels(); if (module === "发现") { show("searchHeader"); show("subNav"); show("videoList"); // 恢复子导航默认选中"推荐" document.querySelectorAll(".sub-nav-btn").forEach(function (btn) { btn.classList.toggle("active", btn.dataset.tab === "recommend"); }); currentSubTab = "recommend"; loadVideos("数人"); return; } else if (module === "知识") { show("sectionTitleBar"); show("shushuoHero"); show("shushuoTopics"); show("shushuoFeed"); document.getElementById("contentTitle").textContent = colors.name; loadKnowledgeCards(); return; } else if (module === "数学人生") { show("scriptSquare"); renderScriptSquare(); return; } else if (module === "我的") { show("sectionTitleBar"); show("profilePage"); document.getElementById("contentTitle").textContent = colors.name; loadProfileData(); return; } } function hideAllPanels() { hide("searchHeader"); hide("subNav"); hide("sectionTitleBar"); hide("lifeSubNav"); hide("moduleBanner"); hide("shushuoHero"); hide("shushuoTopics"); hide("shushuoFeed"); hide("storyContainer"); hide("simulatorContainer"); hide("disciplineGridContainer"); hide("disciplineActiveHeader"); hide("disciplineGrid"); hide("categoryFilter"); hide("profilePage"); hide("scriptSquare"); hide("loadingArea"); hide("videoList"); } function show(id) { var el = document.getElementById(id); if (el) el.style.display = (id === "videoList" ? "grid" : id === "subNav" ? "flex" : "block"); } function hide(id) { var el = document.getElementById(id); if (el) el.style.display = "none"; } function updateBanner(module) { var info = MODULE_BANNERS[module] || MODULE_BANNERS["发现"]; var el = document.getElementById("moduleBanner"); if (!el) return; el.style.display = "flex"; el.className = "module-banner " + (MODULE_CSS_CLASS[module] || "shuren"); el.innerHTML = ''; } // ============================================================ // 数人:搜索、子导航、学科网格 // ============================================================ function handleSearch() { var q = document.getElementById("videoSearch").value.toLowerCase().trim(); if (!q) { renderVideoList(allVideos); return; } renderVideoList(allVideos.filter(function (v) { return (v.title && v.title.toLowerCase().indexOf(q) >= 0) || (v.description && v.description.toLowerCase().indexOf(q) >= 0) || (v.discipline && v.discipline.toLowerCase().indexOf(q) >= 0) || (v.author && v.author.toLowerCase().indexOf(q) >= 0); })); } function switchSubTab(tab) { currentSubTab = tab; document.querySelectorAll(".sub-nav-btn").forEach(function (btn) { btn.classList.toggle("active", btn.dataset.tab === tab); }); // 先把所有内容区重置 var gridCtn = document.getElementById("disciplineGridContainer"); var videoList = document.getElementById("videoList"); var activeHdr = document.getElementById("disciplineActiveHeader"); var catFilter = document.getElementById("categoryFilter"); if (gridCtn) gridCtn.style.display = "none"; if (activeHdr) activeHdr.style.display = "none"; if (catFilter) catFilter.style.display = "none"; if (tab === "discipline") { // 专业分区:显示图标网格 if (videoList) videoList.style.display = "none"; renderDisciplineGrid(); } else if (tab === "hot") { if (videoList) videoList.style.display = "grid"; var sorted = [].concat(allVideos).sort(function (a, b) { return (hashStr(b.title) % 1000) - (hashStr(a.title) % 1000); }); renderVideoList(sorted); } else { // 推荐 if (videoList) videoList.style.display = "grid"; renderVideoList(allVideos); } } function renderDisciplineGrid() { var gridCtn = document.getElementById("disciplineGridContainer"); var grid = document.getElementById("disciplineGrid"); var videoList = document.getElementById("videoList"); if (videoList) videoList.style.display = "none"; var keys = Object.keys(DISCIPLINE_ICONS).filter(function (d) { return d !== "通用"; }); grid.innerHTML = keys.map(function (d) { var info = DISCIPLINE_ICONS[d]; return '
' + '
' + (info.symbol || info.icon) + '
' + '
' + esc(d) + '
'; }).join(""); // 直接设置 display,绕过 show/hide gridCtn.style.display = "block"; grid.style.display = "grid"; } function selectDiscipline(d) { document.getElementById("disciplineGridContainer").style.display = "none"; document.getElementById("categoryFilter").style.display = "none"; document.getElementById("videoList").style.display = "grid"; document.getElementById("disciplineActiveHeader").style.display = "flex"; document.getElementById("activeDisciplineName").textContent = d; renderVideoList(allVideos.filter(function (v) { return v.discipline === d; })); } function backToDisciplineGrid() { document.getElementById("disciplineActiveHeader").style.display = "none"; document.getElementById("categoryFilter").style.display = "none"; renderDisciplineGrid(); } // ============================================================ // 数说:发现Feed(动态版) // ============================================================ async function loadKnowledgeCards(topic) { show("loadingArea"); hide("shushuoFeed"); var url = "/api/knowledge"; if (topic) url += "?topic=" + encodeURIComponent(topic); try { var resp = await fetch(url); var result = await resp.json(); hide("loadingArea"); if (result.code === 0) { allKnowledgeCards = result.data || []; var topics = result.topics || []; renderShushuoTopics(topics); renderShushuoFeed(allKnowledgeCards); } } catch (e) { hide("loadingArea"); } } function renderShushuoTopics(topics) { var scrollEl = document.getElementById("topicsScroll"); scrollEl.innerHTML = ('全部') + topics.map(function (t) { return '' + esc(t) + ''; }).join(""); } function renderShushuoFeed(cards) { show("shushuoFeed"); updateShushuoHero(); var feed = document.getElementById("shushuoFeed"); feed.innerHTML = cards.map(function (c) { var isFav = userFavorites.knowledge[c.id] || false; return '
' + '
' + '
' + (c.icon || "📖") + '
' + '
' + esc(c.question || "") + '
' + '' + '
' + '
' + esc(c.title) + '
' + '
' + esc(c.description) + '
' + '
'; }).join(""); } function filterShushuoByTopic(topic) { document.querySelectorAll(".topic-pill").forEach(function (p) { var is = p.textContent.trim() === (topic || "全部"); p.classList.toggle("active", is); }); if (topic) { loadKnowledgeCards(topic); } else { loadKnowledgeCards(); } document.querySelector(".content-area").scrollTo({ top: 0, behavior: "smooth" }); } function openKnowledgeDetail(cardId) { var card = allKnowledgeCards.find(function (c) { return c.id === cardId; }); if (!card) return; setAIContext("knowledge", card.title); recordHistory("knowledge", card.id, card.title, card.icon || "📖"); // 在聊天中询问 openChat(); quickAsk(card.title + "是什么?能给我详细讲讲吗?"); } async function toggleFavorite(type, targetId, title, cover) { var isFav = (userFavorites[type] || {})[targetId]; if (isFav) { // 取消收藏 try { await fetch("/api/favorites", { method: "DELETE", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ user_id: userId, target_type: type, target_id: targetId }) }); if (userFavorites[type]) delete userFavorites[type][targetId]; showToast("已取消收藏"); } catch (e) { /* ignore */ } } else { // 添加收藏 try { await fetch("/api/favorites", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ user_id: userId, target_type: type, target_id: targetId, title: title, cover: cover }) }); if (!userFavorites[type]) userFavorites[type] = {}; userFavorites[type][targetId] = true; showToast("已加入收藏"); } catch (e) { /* ignore */ } } // 刷新当前视图 if (currentModule === "知识" && type === "knowledge") { renderShushuoFeed(allKnowledgeCards); } else if (currentModule === "发现" && type === "video") { renderVideoList(allVideos); } } async function loadUserFavorites() { try { var resp = await fetch("/api/favorites?user_id=" + userId); var result = await resp.json(); if (result.code === 0) { userFavorites = { video: {}, knowledge: {} }; (result.data || []).forEach(function (f) { if (!userFavorites[f.target_type]) userFavorites[f.target_type] = {}; userFavorites[f.target_type][f.target_id] = true; }); } } catch (e) { userFavorites = { video: {}, knowledge: {} }; } } async function recordHistory(type, targetId, title, cover) { try { await fetch("/api/history", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ user_id: userId, target_type: type, target_id: targetId, title: title, cover: cover }) }); } catch (e) { /* ignore */ } } function showToast(message) { var toast = document.getElementById("globalToast"); if (!toast) { toast = document.createElement("div"); toast.id = "globalToast"; toast.style.cssText = "position:fixed;bottom:100px;left:50%;transform:translateX(-50%);background:rgba(0,0,0,0.78);color:#fff;padding:8px 20px;border-radius:20px;font-size:13px;z-index:9999;pointer-events:none;transition:opacity 0.3s;opacity:0;"; document.body.appendChild(toast); } toast.textContent = message; toast.style.opacity = "1"; clearTimeout(toast._timer); toast._timer = setTimeout(function () { toast.style.opacity = "0"; }, 1500); } // ============================================================ // 个人中心 // ============================================================ async function loadProfileData() { try { var [statsResp, profileResp] = await Promise.all([ fetch("/api/user/stats?user_id=" + userId), fetch("/api/user/profile?user_id=" + userId) ]); var stats = (await statsResp.json()).data || { watch_count: 0, favorite_count: 0 }; var profile = (await profileResp.json()).data || {}; // 更新统计数据 var statNums = document.querySelectorAll(".profile-stat-num"); if (statNums.length >= 3) { statNums[0].textContent = stats.watch_count || 0; statNums[1].textContent = stats.favorite_count || 0; statNums[2].textContent = "2"; // 剧情存档数保持固定 } // 更新头像和昵称 var avatar = document.getElementById("profileAvatar"); if (avatar) { avatar.textContent = (profile.avatar_emoji || userAvatar); avatar.style.background = (profile.avatar_color || userAvatarColor); userAvatar = profile.avatar_emoji || userAvatar; userAvatarColor = profile.avatar_color || userAvatarColor; } var nameEl = document.getElementById("profileName"); if (nameEl) nameEl.textContent = (profile.nickname || "游客用户"); var idEl = document.getElementById("profileId"); if (idEl) idEl.textContent = "ID: " + userId.substring(0, 14); } catch (e) { /* ignore */ } } function showFavorites() { loadFavoritesList(); } async function loadFavoritesList() { showProfileListPanel("我的收藏"); var panel = document.getElementById("profileListContent"); panel.innerHTML = '
'; try { var resp = await fetch("/api/favorites?user_id=" + userId); var result = await resp.json(); var items = result.data || []; if (items.length === 0) { panel.innerHTML = '

还没有收藏

去发现有趣的内容吧

'; } else { panel.innerHTML = items.map(function (f) { return '
' + '
' + (f.cover || "◎") + '
' + '
' + esc(f.title) + '
' + f.target_type + ' · ' + (f.created_at || "").substring(0, 10) + '
' + '' + '
'; }).join(""); } } catch (e) { panel.innerHTML = '
⚠️

加载失败

'; } } function showHistory() { loadHistoryList(); } async function loadHistoryList() { showProfileListPanel("观看记录"); var panel = document.getElementById("profileListContent"); panel.innerHTML = '
'; try { var resp = await fetch("/api/history?user_id=" + userId + "&limit=30"); var result = await resp.json(); var items = result.data || []; if (items.length === 0) { panel.innerHTML = '
👀

还没有观看记录

去看看视频和知识卡片吧

'; } else { panel.innerHTML = items.map(function (h) { return '
' + '
' + (h.cover || "◎") + '
' + '
' + esc(h.title) + '
' + h.target_type + ' · ' + (h.watched_at || "").substring(0, 16) + '
' + '
'; }).join(""); } } catch (e) { panel.innerHTML = '
⚠️

加载失败

'; } } function showProfileListPanel(title) { var panel = document.getElementById("profileListPanel"); var titleEl = document.getElementById("profileListTitle"); if (titleEl) titleEl.textContent = title; if (panel) panel.style.display = "block"; // 隐藏个人中心其他内容 var profilePage = document.getElementById("profilePage"); if (profilePage) profilePage.style.display = "none"; } function hideProfileListPanel() { var panel = document.getElementById("profileListPanel"); if (panel) panel.innerHTML = ""; if (panel) panel.style.display = "none"; var profilePage = document.getElementById("profilePage"); if (profilePage) profilePage.style.display = "block"; loadProfileData(); } async function removeFavorite(type, targetId) { await toggleFavorite(type, targetId, "", ""); loadFavoritesList(); loadProfileData(); } function editNickname() { var name = prompt("输入新昵称:", document.querySelector(".profile-name").textContent); if (name && name.trim()) { var trimmed = name.trim().substring(0, 12); fetch("/api/user/profile", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ user_id: userId, nickname: trimmed }) }).then(function () { var el = document.querySelector(".profile-name"); if (el) el.textContent = trimmed; showToast("昵称已更新"); }); } } function updateShushuoHero() { document.getElementById("heroTitle").textContent = "数学不是公式,是你理解世界的方式"; document.getElementById("heroDesc").textContent = "每天一个数学概念,让思维变得更清晰"; var el = document.querySelector(".hero-stats"); if (el) el.innerHTML = "📖 " + allKnowledgeCards.length + "个概念⏱ 阅读约" + (allKnowledgeCards.length * 5) + "分钟"; } function adjustColor(hex, amount) { var num = parseInt(hex.replace("#",""), 16); var r = Math.min(255, (num >> 16) + amount); var g = Math.min(255, ((num >> 8) & 0x00FF) + amount); var b = Math.min(255, (num & 0x0000FF) + amount); return "#" + ((r << 16) | (g << 8) | b).toString(16).padStart(6, "0"); } // ============================================================ // 视频加载与渲染 // ============================================================ async function loadVideos(module, discipline) { show("loadingArea"); document.getElementById("videoList").innerHTML = ""; var url = "/api/videos?module=" + encodeURIComponent(module); if (discipline && discipline !== "all") url += "&discipline=" + encodeURIComponent(discipline); try { var resp = await fetch(url); var result = await resp.json(); hide("loadingArea"); allVideos = result.data || []; if (allVideos.length > 0) { renderVideoList(allVideos); } else { document.getElementById("videoList").innerHTML = '
📭

暂无相关视频

后续将由各学院志愿者上传

'; } } catch (e) { hide("loadingArea"); document.getElementById("videoList").innerHTML = '
⚠️

加载失败

请稍后重试

'; } } function renderVideoList(videos) { var list = document.getElementById("videoList"); if (!videos || videos.length === 0) { list.innerHTML = '
📭

暂无相关视频

'; return; } list.style.display = "grid"; list.innerHTML = videos.map(function (v) { var avatarColor = AVATAR_COLORS[hashStr(v.author || "") % AVATAR_COLORS.length]; var initial = (v.author || "?").replace(/学院|·|同学|系/g, "").slice(0, 1); var dur = v.duration_min || 5; var icon = v.discipline_icon && v.discipline_icon !== "·" ? v.discipline_icon : "◎"; var coverBg = "hsl(" + (hashStr(v.discipline || "") % 360) + ", 40%, 90%)"; var isFav = userFavorites.video[v.id] || false; return '
' + '
' + icon + '
' + dur + ':00
' + '' + '
' + '
' + esc(v.title) + '
' + '
' + initial + '' + '' + esc(v.author) + '' + '
👁️' + ((hashStr(v.title) % 1000) + 100) + '
'; }).join(""); } function openVideo(id, title, desc, url, author, cat) { currentVideoUrl = url; currentVideoTitle = title; document.getElementById("videoModalTitle").textContent = title; document.getElementById("videoModalDesc").textContent = desc; document.getElementById("videoModalAuthor").innerHTML = author; document.getElementById("videoModalCat").innerHTML = cat || "通用"; document.getElementById("videoLinkBtn").style.display = url ? "block" : "none"; document.getElementById("videoPlaceholderText").textContent = url ? "点击下方按钮查看完整课程视频" : "视频链接待补充"; // 收藏状态 var favBtn = document.getElementById("videoModalFav"); var vid = parseInt(id); favBtn.textContent = (userFavorites.video[vid]) ? "★ 已收藏" : "☆ 收藏"; favBtn.dataset.vid = vid; document.getElementById("videoModal").classList.add("show"); // AI上下文 setAIContext("video", title); // 记录观看历史 recordHistory("video", vid, title, cat || "◎"); } function closeVideo() { document.getElementById("videoModal").classList.remove("show"); } function openVideoLink() { if (currentVideoUrl) window.open(currentVideoUrl, "_blank"); } function toggleVideoFav() { var btn = document.getElementById("videoModalFav"); var vid = parseInt(btn.dataset.vid); var title = currentVideoTitle; toggleFavorite("video", vid, title, "◎"); btn.textContent = (userFavorites.video[vid]) ? "★ 已收藏" : "☆ 收藏"; } // ============================================================ // 视觉小说引擎 (数思) — 唯一引擎 // ============================================================ async function loadStoryData() { try { var resp = await fetch("/api/story"); var result = await resp.json(); if (result.code === 0) storyData = result.data; } catch (e) { console.error("故事数据加载失败:", e); } } function loadVNSave() { try { var s = localStorage.getItem("mathAppVNSave"); if (s) vnState = JSON.parse(s); } catch (e) {} } function saveVNState() { localStorage.setItem("mathAppVNSave", JSON.stringify(vnState)); alert("进度已存档!"); } function resetVNSave() { var startNode = (storyData && storyData.start_node) || "ch1_start"; vnState = { currentNode: startNode, stats: { shuli: 10, xiayi: 10, xinzhi: 10 }, favorability: { "欧阳侠": 0, "Eddie侠": 0, "厚朴侠": 0, "三力思侠": 0, "溜溜侠": 0, "孔锝侠": 0, "星侠": 0, "甲亢侠": 0 } }; localStorage.removeItem("mathAppVNSave"); navigateVN(startNode); } function applyVNStats(effects) { if (!effects) return; if (effects.shuli) vnState.stats.shuli = (vnState.stats.shuli || 10) + effects.shuli; if (effects.xiayi) vnState.stats.xiayi = (vnState.stats.xiayi || 10) + effects.xiayi; if (effects.xinzhi) vnState.stats.xinzhi = (vnState.stats.xinzhi || 10) + effects.xinzhi; // 兼容旧属性名 if (effects.intelligence !== undefined) vnState.stats.shuli = (vnState.stats.shuli || 10) + effects.intelligence; if (effects.charm !== undefined) vnState.stats.xiayi = (vnState.stats.xiayi || 10) + effects.charm; if (effects.stress !== undefined) vnState.stats.xinzhi = (vnState.stats.xinzhi || 10) - effects.stress; // 好感度 if (effects.favorability) { if (!vnState.favorability) vnState.favorability = {}; for (var char in effects.favorability) { vnState.favorability[char] = (vnState.favorability[char] || 0) + effects.favorability[char]; } } } function startStoryEngine() { setAIContext("story", "葫芦侠行"); var startNode = (storyData && storyData.start_node) || "ch1_start"; // 如果存档中有旧节点但新JSON里不存在,回退到起始节点 var savedNode = vnState.currentNode; if (savedNode && storyData && storyData.nodes && storyData.nodes[savedNode]) { navigateVN(savedNode); } else { vnState.currentNode = startNode; navigateVN(startNode); } } function navigateVN(nodeId) { if (!storyData || !storyData.nodes || !storyData.nodes[nodeId]) { document.getElementById("storyContainer").innerHTML = '

剧情加载失败

节点未找到

'; return; } var node = storyData.nodes[nodeId]; vnState.currentNode = nodeId; var container = document.getElementById("storyContainer"); if (!container) return; container.style.display = "block"; container.scrollIntoView({ behavior: "smooth" }); // 构建完整 HTML var ch = detectChapter(nodeId); var chNames = ["","初入江湖","西部教习","数理论道","脑学初创","报国之路","暗流涌动","陋室修炼","真相之门"]; var chName = chNames[ch] || ""; var html = ''; html += '
'; html += '
第' + ch + '章 · ' + chName + '
'; html += '
'; html += '📐 数理根基: ' + (vnState.stats.shuli || 10) + ''; html += '⚔️ 侠义声望: ' + (vnState.stats.xiayi || 10) + ''; html += '🧘 心志韧性: ' + (vnState.stats.xinzhi || 10) + ''; html += '
'; html += '
'; html += ''; html += ''; html += ''; html += '
'; // 场景背景 + 立绘 var bgKey = CHAPTER_BG[ch] || "紫金山"; var bgUrl = BG_MAP[bgKey] || ""; if (node.background) bgUrl = BG_MAP[node.background] || node.background || bgUrl; // 立绘:优先 node.sprite,其次从 SPRITE_MAP 查找角色名 var spriteUrl = ""; var characterName = node.character || ""; if (node.sprite) { spriteUrl = node.sprite; } else if (characterName) { // 匹配 SPRITE_MAP 中的角色(支持多角色用 & 分隔) var names = characterName.split("&"); for (var ni = 0; ni < names.length; ni++) { var n = names[ni].trim(); if (SPRITE_MAP[n]) { spriteUrl = SPRITE_MAP[n]; break; } } } html += '
'; if (bgUrl) html += ''; if (spriteUrl) html += '' + esc(characterName) + ''; html += '
'; if (characterName) html += '
' + esc(characterName) + '
'; html += '
'; html += '
'; html += '
'; container.innerHTML = html; // 打字机效果 var sceneText = document.getElementById("sceneText"); var text = node.scene || ""; var i = 0; function typeNext() { if (i < text.length) { sceneText.textContent += text.charAt(i); i++; setTimeout(typeNext, 25); } else { buildChoiceButtons(node.choices || []); } } sceneText.textContent = ""; typeNext(); } function buildChoiceButtons(choices) { var area = document.getElementById("choicesArea"); if (!area) return; area.innerHTML = ""; choices.forEach(function (c, idx) { var btn = document.createElement("button"); btn.className = "choice-btn"; btn.style.animationDelay = (idx * 0.1) + "s"; // 效果文本 var effectsHTML = ""; if (c.effects) { var parts = []; if (c.effects.shuli) parts.push("数理根基" + (c.effects.shuli > 0 ? "+" : "") + c.effects.shuli); if (c.effects.xiayi) parts.push("侠义声望" + (c.effects.xiayi > 0 ? "+" : "") + c.effects.xiayi); if (c.effects.xinzhi) parts.push("心志韧性" + (c.effects.xinzhi > 0 ? "+" : "") + c.effects.xinzhi); // 兼容旧属性 if (c.effects.intelligence) parts.push("数理根基" + (c.effects.intelligence > 0 ? "+" : "") + c.effects.intelligence); if (c.effects.charm) parts.push("侠义声望" + (c.effects.charm > 0 ? "+" : "") + c.effects.charm); if (c.effects.stress) parts.push("心志韧性" + (c.effects.stress > 0 ? "+" : "") + c.effects.stress); if (c.effects.favorability) { for (var char in c.effects.favorability) { var val = c.effects.favorability[char]; parts.push(char + "好感" + (val > 0 ? "+" : "") + val); } } if (parts.length > 0) effectsHTML = '[' + parts.join(" | ") + ']'; } btn.innerHTML = '' + esc(c.text) + '' + effectsHTML; btn.onclick = (function (nextNode, effects) { return function () { applyVNStats(effects); navigateVN(nextNode); }; })(c.next, c.effects); area.appendChild(btn); }); } // ============================================================ // AI 聊天 // ============================================================ function openChat() { document.getElementById("chatOverlay").classList.add("show"); document.getElementById("chatMessages").scrollTop = document.getElementById("chatMessages").scrollHeight; } function closeChat() { document.getElementById("chatOverlay").classList.remove("show"); } function quickAsk(q) { document.getElementById("chatInput").value = q; sendMessage(); } async function sendMessage() { var input = document.getElementById("chatInput"); var message = input.value.trim(); if (!message) return; appendChat("user", message); input.value = ""; var typingId = appendTyping(); try { var resp = await fetch("/api/chat", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ message: message, context: currentContext }) }); var result = await resp.json(); removeTyping(typingId); appendChat("assistant", result.code === 0 ? result.reply : "抱歉,AI 助教暂时无法回复。"); } catch (e) { removeTyping(typingId); appendChat("assistant", "网络异常,请稍后重试。"); } } function setAIContext(type, title) { currentContext = { type: type, title: title || "" }; var hint = document.getElementById("chatContextHint"); if (!hint) { hint = document.createElement("div"); hint.id = "chatContextHint"; hint.style.cssText = "padding:6px 12px;font-size:11px;color:var(--accent);background:var(--accent-light);text-align:center;"; var chatPanel = document.querySelector(".chat-panel"); var header = document.querySelector(".chat-header"); if (header && chatPanel) chatPanel.insertBefore(hint, header.nextSibling); } hint.textContent = currentContext.title ? "📍 当前:" + currentContext.title : ""; hint.style.display = currentContext.title ? "block" : "none"; } function appendChat(role, text) { var container = document.getElementById("chatMessages"); var div = document.createElement("div"); div.className = "chat-msg " + role; div.innerHTML = '
' + esc(text) + "
"; container.appendChild(div); container.scrollTop = container.scrollHeight; } function appendTyping() { var container = document.getElementById("chatMessages"); var div = document.createElement("div"); div.className = "chat-msg assistant typing-msg"; div.innerHTML = '
'; container.appendChild(div); container.scrollTop = container.scrollHeight; return div; } function removeTyping(el) { if (el && el.parentNode) el.remove(); } // ============================================================ // 数学人生子标签切换 // ============================================================ function switchLifeTab(tab) { currentLifeTab = tab; document.querySelectorAll(".life-sub-btn").forEach(function (b) { b.classList.toggle("active", b.dataset.life === tab); }); if (tab === "simulator") { hide("storyContainer"); show("simulatorContainer"); setAIContext("simulator", "数学专业模拟器"); startSimulator(); } else { hide("simulatorContainer"); show("storyContainer"); setAIContext("story", "葫芦侠行"); startStoryEngine(); } } // ============================================================ // 数学专业模拟器引擎 // ============================================================ async function loadSimulatorData() { try { var resp = await fetch("/api/simulator"); var result = await resp.json(); if (result.code === 0) simulatorData = result.data; } catch (e) { console.error("模拟器数据加载失败:", e); } } async function loadSimSave() { try { var resp = await fetch("/api/simulator/save?user_id=" + userId); var result = await resp.json(); if (result.code === 0 && result.data) { simState = result.data; } } catch (e) {} } function saveSimState() { simState.history.push(simState.currentNode); fetch("/api/simulator/save", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ user_id: userId, state: simState }) }).catch(function () {}); } function resetSimulator() { simState = { currentNode: (simulatorData && simulatorData.start_node) || "y1_welcome", stats: { gpa: 10, interest: 10, skill: 10 }, history: [] }; saveSimState(); navigateSimulator(simState.currentNode); } async function startSimulator() { if (!simulatorData) await loadSimulatorData(); await loadSimSave(); if (!simState.currentNode || !simulatorData || !simulatorData.nodes || !simulatorData.nodes[simState.currentNode]) { resetSimulator(); } else { navigateSimulator(simState.currentNode); } } function navigateSimulator(nodeId) { if (!simulatorData || !simulatorData.nodes || !simulatorData.nodes[nodeId]) { document.getElementById("simulatorContainer").innerHTML = '

剧情加载失败

'; return; } var node = simulatorData.nodes[nodeId]; simState.currentNode = nodeId; saveSimState(); var container = document.getElementById("simulatorContainer"); if (!container) return; container.style.display = "block"; // 如果遇到结局节点 if (node.end) { showEnding(node); return; } container.scrollIntoView({ behavior: "smooth" }); var ch = (node.year || 1); var stage = node.stage || ""; var html = ''; html += '
'; html += '
🎓 大' + ["", "一", "二", "三", "四"][ch] + ' · ' + stage + '
'; html += '
'; Object.keys(simState.stats).forEach(function (k) { html += '' + SIM_STAT_EMOJIS[k] + ' ' + SIM_STAT_LABELS[k] + ': ' + simState.stats[k] + ''; }); html += '
'; html += '
'; html += ''; html += ''; html += ''; html += ''; html += '
'; html += '
'; if (node.character) html += '
' + esc(node.character) + '
'; html += '
'; html += '
'; html += '
'; container.innerHTML = html; // 打字机效果 var textEl = document.getElementById("simText"); var text = node.scene || ""; var i = 0; function typeNext() { if (i < text.length) { textEl.textContent += text.charAt(i); i++; setTimeout(typeNext, 20); } else { buildSimChoices(node.choices || []); } } textEl.textContent = ""; typeNext(); } function buildSimChoices(choices) { var area = document.getElementById("simChoicesArea"); if (!area) return; area.innerHTML = ""; choices.forEach(function (c, idx) { var btn = document.createElement("button"); btn.className = "sim-choice-btn"; btn.style.animationDelay = (idx * 0.1) + "s"; var effectsHTML = ""; if (c.effects) { var parts = []; if (c.effects.gpa) parts.push((c.effects.gpa > 0 ? "+" : "") + c.effects.gpa + " 绩点"); if (c.effects.interest) parts.push((c.effects.interest > 0 ? "+" : "") + c.effects.interest + " 兴趣"); if (c.effects.skill) parts.push((c.effects.skill > 0 ? "+" : "") + c.effects.skill + " 技能"); if (parts.length > 0) effectsHTML = '[' + parts.join(" | ") + ']'; } if (c.hint) effectsHTML += '💡 ' + esc(c.hint) + ''; btn.innerHTML = '' + esc(c.text) + '' + effectsHTML; btn.onclick = (function (nextNode, effects) { return function () { if (effects) applySimStats(effects); navigateSimulator(nextNode); }; })(c.next, c.effects); area.appendChild(btn); }); } function applySimStats(effects) { if (!effects) return; if (effects.gpa) simState.stats.gpa = Math.max(0, Math.min(30, (simState.stats.gpa || 10) + effects.gpa)); if (effects.interest) simState.stats.interest = Math.max(0, Math.min(30, (simState.stats.interest || 10) + effects.interest)); if (effects.skill) simState.stats.skill = Math.max(0, Math.min(30, (simState.stats.skill || 10) + effects.skill)); } function showEnding(node) { document.getElementById("endingOverlay").classList.add("show"); var badges = { pure_math: "🔮", ai: "🤖", finance: "💰", research: "🔬", startup: "🚀", teacher: "📚", engineer: "⚙️", security: "🔒" }; document.getElementById("endingBadge").textContent = badges[node.ending_type] || "🎓"; document.getElementById("endingType").textContent = node.character || "结局"; document.getElementById("endingScene").textContent = node.scene || ""; document.getElementById("endingTitle").textContent = node.stage || ""; // 记录解锁结局 if (node.ending_type) unlockEnding(node.ending_type); // 统计数据 var statsHtml = '
'; Object.keys(simState.stats).forEach(function (k) { statsHtml += '
' + simState.stats[k] + '
' + SIM_STAT_LABELS[k] + '
'; }); statsHtml += '
'; document.getElementById("endingStats").innerHTML = statsHtml; } function closeEnding() { document.getElementById("endingOverlay").classList.remove("show"); } // ============================================================ // 头像选择器 // ============================================================ var PICKER_EMOJIS = ["π","∑","∫","∞","Δ","∂","α","β","γ","λ","μ","σ","Ω","∇","⊥","⊕","⊗","≈","±","÷","√","∏","∃","∀","∈","⊂","∪","∩","ℵ","∮"]; function openAvatarPicker() { document.getElementById("avatarPickerOverlay").classList.add("show"); // 渲染emoji网格 var grid = document.getElementById("pickerEmojiGrid"); grid.innerHTML = PICKER_EMOJIS.map(function (e) { var selected = e === userAvatar ? ' style="border-color:var(--accent);background:var(--accent-light)"' : ""; return ''; }).join(""); // 渲染颜色行 var colors = ["#A0B9D8","#B8D0C9","#DAB8B0","#C5CAD0","#B8A99A","#9BB8AE","#ACAEC5","#C4B0B8"]; var colorRow = document.getElementById("pickerColorRow"); colorRow.innerHTML = colors.map(function (c) { var selected = c === userAvatarColor ? ' style="box-shadow:0 0 0 4px ' + c + '44"' : ""; return ''; }).join(""); } function selectAvatar(emoji) { userAvatar = emoji; localStorage.setItem("mathAppAvatar", emoji); openAvatarPicker(); // refresh grid updateAvatarUI(); fetch("/api/user/profile", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ user_id: userId, avatar_emoji: emoji }) }); } function selectAvatarColor(color) { userAvatarColor = color; localStorage.setItem("mathAppAvatarColor", color); openAvatarPicker(); updateAvatarUI(); fetch("/api/user/profile", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ user_id: userId, avatar_color: color }) }); } function updateAvatarUI() { var av = document.getElementById("profileAvatar"); if (av) { av.textContent = userAvatar; av.style.background = userAvatarColor; } var brandIcon = document.querySelector(".brand-icon-sm"); if (brandIcon) { brandIcon.textContent = "葫"; brandIcon.style.background = userAvatarColor; } } function closeAvatarPicker(e) { if (e && e.target !== document.getElementById("avatarPickerOverlay")) return; document.getElementById("avatarPickerOverlay").classList.remove("show"); } // ---- 工具函数 ---- function esc(text) { if (!text) return ""; var div = document.createElement("div"); div.textContent = text; return div.innerHTML; } function hashStr(str) { var hash = 0; for (var i = 0; i < str.length; i++) { hash = ((hash << 5) - hash) + str.charCodeAt(i); hash |= 0; } return Math.abs(hash); } // ============================================================ // 剧本广场 (Script Square) // ============================================================ function renderScriptSquare() { try { // 渲染精选剧本大卡 var heroCards = document.getElementById("ssHeroCards"); if (!heroCards) return; heroCards.innerHTML = SCRIPTS.map(function (s) { var statusLabel = s.status === "beta" ? "开发中 · 抢先体验" : "已上线"; return '
' + '
' + '
' + '
' + s.icon + '
' + '
' + esc(s.name) + '
' + '
' + esc(s.desc) + '
' + '
' + s.meta + '' + statusLabel + '
' + '
'; }).join(""); // 渲染即将上线小卡 var comingCards = document.getElementById("ssComingCards"); if (comingCards) { comingCards.innerHTML = COMING_SOON.map(function (c) { return '
' + '
' + c.icon + '
' + '
' + '
' + esc(c.name) + '
' + '
' + esc(c.desc) + '
' + '敬请期待' + '
'; }).join(""); } // 检查是否有可继续的存档 updateContinueCard(); } catch (e) { console.error("剧本广场渲染失败:", e); } } function updateContinueCard() { try { var continueEl = document.getElementById("ssContinue"); if (!continueEl) return; var vnSavedNode = vnState.currentNode; var simSavedNode = simState.currentNode; // 优先显示葫芦侠行存档,其次模拟器存档 if (vnSavedNode && storyData && storyData.nodes && storyData.nodes[vnSavedNode]) { var ch = detectChapter(vnSavedNode); var chNames = ["","初入江湖","西部教习","数理论道","脑学初创","报国之路","暗流涌动","陋室修炼","真相之门"]; document.getElementById("ssContinueTitle").textContent = "葫芦侠行 · 第" + ch + "章"; document.getElementById("ssContinueSub").textContent = chNames[ch] || ""; document.getElementById("ssContinueCard").onclick = function () { enterScript("huluxiaxing"); }; continueEl.style.display = "block"; } else if (simSavedNode && simulatorData && simulatorData.nodes && simulatorData.nodes[simSavedNode]) { var node = simulatorData.nodes[simSavedNode]; document.getElementById("ssContinueTitle").textContent = "数学专业模拟器"; document.getElementById("ssContinueSub").textContent = (node.stage || "") + (node.year ? " · 大" + ["","一","二","三","四"][node.year] : ""); document.getElementById("ssContinueCard").onclick = function () { enterScript("simulator"); }; continueEl.style.display = "block"; } else { continueEl.style.display = "none"; } } catch (e) { console.error("继续卡片更新失败:", e); } } function enterScript(scriptId) { hide("scriptSquare"); if (scriptId === "huluxiaxing") { setAIContext("story", "葫芦侠行"); hide("simulatorContainer"); show("lifeSubNav"); show("storyContainer"); var btns = document.querySelectorAll(".life-sub-btn"); btns.forEach(function (b) { b.classList.toggle("active", b.dataset.life === "story"); }); startStoryEngine(); } else if (scriptId === "simulator") { setAIContext("simulator", "数学专业模拟器"); hide("storyContainer"); show("lifeSubNav"); show("simulatorContainer"); var btns = document.querySelectorAll(".life-sub-btn"); btns.forEach(function (b) { b.classList.toggle("active", b.dataset.life === "simulator"); }); startSimulator(); } else if (scriptId === "mathbti") { // 打开 MathBTI 独立页面 window.location.href = "/mathbti"; } } function backToSquare() { hide("lifeSubNav"); hide("storyContainer"); hide("simulatorContainer"); show("scriptSquare"); renderScriptSquare(); document.querySelector(".content-area").scrollTo({ top: 0, behavior: "smooth" }); } // ============================================================ // 存档系统 (Save System — v8.3) // ============================================================ function getSaveKey(scriptId) { return "mathAppSave_" + scriptId; } function loadAllSaves(scriptId) { try { var raw = localStorage.getItem(getSaveKey(scriptId)); return raw ? JSON.parse(raw) : []; } catch (e) { return []; } } function saveAllSaves(scriptId, saves) { localStorage.setItem(getSaveKey(scriptId), JSON.stringify(saves)); } function openSavePanel() { var scriptId = (currentLifeTab === "simulator") ? "simulator" : "huluxiaxing"; document.getElementById("savePanelOverlay").classList.add("show"); renderSaveSlots(scriptId); } function closeSavePanel() { document.getElementById("savePanelOverlay").classList.remove("show"); } function renderSaveSlots(scriptId) { var slots = document.getElementById("saveSlots"); var saves = loadAllSaves(scriptId); var MAX_SLOTS = 5; var html = ""; for (var i = 0; i < MAX_SLOTS; i++) { var s = saves[i]; if (s) { // 已使用的槽位 var icon = scriptId === "simulator" ? "🎓" : "📖"; var name = (scriptId === "simulator") ? "数学专业模拟器" : "葫芦侠行"; var meta = s.chapter || ""; var date = s.savedAt ? s.savedAt.substring(0, 16).replace("T", " ") : ""; html += '
'; html += '
' + icon + '
'; html += '
'; html += '
' + esc(name) + '
'; html += '
' + esc(meta) + '' + date + '
'; html += '
'; html += '
'; html += ''; html += ''; html += '
'; } else { // 空槽位 html += '
'; html += '+ 空槽位 ' + (i + 1) + ' — 点击保存'; html += '
'; } } slots.innerHTML = html; } function saveToSlot(scriptId, slotIndex) { var saveData = { savedAt: new Date().toISOString(), scriptId: scriptId }; if (scriptId === "huluxiaxing") { saveData.chapter = detectChapter(vnState.currentNode) ? "第" + detectChapter(vnState.currentNode) + "章" : ""; saveData.nodeId = vnState.currentNode; saveData.stats = JSON.parse(JSON.stringify(vnState.stats)); saveData.favorability = JSON.parse(JSON.stringify(vnState.favorability || {})); saveData.vnState = JSON.parse(JSON.stringify(vnState)); } else { var node = simulatorData && simulatorData.nodes && simulatorData.nodes[simState.currentNode]; saveData.chapter = (node ? (node.stage || "大" + node.year) : ""); saveData.nodeId = simState.currentNode; saveData.stats = JSON.parse(JSON.stringify(simState.stats)); saveData.simState = JSON.parse(JSON.stringify(simState)); } var saves = loadAllSaves(scriptId); saves[slotIndex] = saveData; saveAllSaves(scriptId, saves); renderSaveSlots(scriptId); showToast("存档成功 ✓"); } function overwriteSlot(scriptId, slotIndex) { saveToSlot(scriptId, slotIndex); } function loadFromSlot(scriptId, slotIndex) { var saves = loadAllSaves(scriptId); var saveData = saves[slotIndex]; if (!saveData) return; if (scriptId === "huluxiaxing") { if (saveData.vnState) { vnState = JSON.parse(JSON.stringify(saveData.vnState)); localStorage.setItem("mathAppVNSave", JSON.stringify(vnState)); } hide("scriptSquare"); hide("simulatorContainer"); show("lifeSubNav"); show("storyContainer"); document.querySelectorAll(".life-sub-btn").forEach(function (b) { b.classList.toggle("active", b.dataset.life === "story"); }); currentLifeTab = "story"; startStoryEngine(); } else { if (saveData.simState) { simState = JSON.parse(JSON.stringify(saveData.simState)); saveSimState(); } hide("scriptSquare"); hide("storyContainer"); show("lifeSubNav"); show("simulatorContainer"); document.querySelectorAll(".life-sub-btn").forEach(function (b) { b.classList.toggle("active", b.dataset.life === "simulator"); }); currentLifeTab = "simulator"; startSimulator(); } closeSavePanel(); showToast("读档成功 ✓"); } function deleteSlot(scriptId, slotIndex) { var saves = loadAllSaves(scriptId); saves[slotIndex] = null; // 保留空位 saveAllSaves(scriptId, saves); renderSaveSlots(scriptId); showToast("已删除存档"); } // 兼容旧版存档按钮 function saveVNState() { var scriptId = "huluxiaxing"; // 自动保存到槽位0 saveToSlot(scriptId, 0); } // ============================================================ // 结局画廊 (Ending Gallery — v8.3) // ============================================================ const ALL_ENDINGS = [ { id: "truth", name: "真相守望", icon: "🔮", desc: "成为葫芦数学世界首位居民与共创者" }, { id: "master", name: "数理宗师", icon: "📚", desc: "葫芦侠成为一代宗师,数堂发扬光大" }, { id: "wander", name: "逍遥江湖", icon: "🍃", desc: "远离纷争,在江湖中自由游历" }, { id: "sail", name: "出海远航", icon: "⛵", desc: "随欧阳侠远赴新大陆,开创东西方交流" }, { id: "hero", name: "悲情英雄", icon: "⚔️", desc: "坚守信念,孤身面对审查与命运" }, { id: "fall", name: "堕落之路", icon: "🌑", desc: "在权力与诱惑中迷失自我" }, { id: "jail", name: "冤狱", icon: "⛓", desc: "被冤入狱,含恨而终" }, { id: "void", name: "虚无", icon: "🌫", desc: "发现真相后精神崩溃,消失于江湖" }, { id: "math_phd", name: "数学博士", icon: "🔬", desc: "坚持学术之路,走向研究巅峰" }, { id: "quant", name: "量化交易员", icon: "💰", desc: "用数学模型在金融市场叱咤风云" }, { id: "teacher", name: "高中教师", icon: "📝", desc: "回到家乡,培养下一代数学人才" }, { id: "data_sci", name: "数据科学家", icon: "📊", desc: "在互联网公司用数据改变世界" } ]; function getUnlockedEndings() { try { var raw = localStorage.getItem("mathAppUnlockedEndings"); return raw ? JSON.parse(raw) : []; } catch (e) { return []; } } function unlockEnding(endingId) { var endings = getUnlockedEndings(); if (endings.indexOf(endingId) < 0) { endings.push(endingId); localStorage.setItem("mathAppUnlockedEndings", JSON.stringify(endings)); } } function openGallery() { document.getElementById("galleryOverlay").classList.add("show"); renderGallery(); } function closeGallery() { document.getElementById("galleryOverlay").classList.remove("show"); } function renderGallery() { var unlocked = getUnlockedEndings(); var progressEl = document.getElementById("galleryProgress"); progressEl.textContent = unlocked.length + "/" + ALL_ENDINGS.length; var grid = document.getElementById("galleryGrid"); grid.innerHTML = ALL_ENDINGS.map(function (e) { var isUnlocked = unlocked.indexOf(e.id) >= 0; if (isUnlocked) { return ''; } else { return ''; } }).join(""); }