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

1510 lines
64 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* =========================================================
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 = '<span class="banner-icon">' + info.icon + '</span><div class="banner-text"><div class="banner-title">' + info.title + '</div><div class="banner-sub">' + info.sub + '</div></div>';
}
// ============================================================
// 数人:搜索、子导航、学科网格
// ============================================================
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 '<div class="discipline-item" onclick="selectDiscipline(\'' + esc(d) + '\')">'
+ '<div class="discipline-icon-box" style="background:' + info.bg + ';color:' + info.color + '">' + (info.symbol || info.icon) + '</div>'
+ '<div class="discipline-label">' + esc(d) + '</div></div>';
}).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 = ('<span class="topic-pill active" onclick="filterShushuoByTopic(\'\')">全部</span>') +
topics.map(function (t) {
return '<span class="topic-pill" onclick="filterShushuoByTopic(\'' + esc(t) + '\')">' + esc(t) + '</span>';
}).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 '<div class="feed-card" onclick="openKnowledgeDetail(' + c.id + ')">'
+ '<div class="feed-card-visual" style="background:linear-gradient(135deg, ' + (c.color || "#3B82F6") + ', ' + adjustColor((c.color || "#3B82F6"), 20) + ')">'
+ '<div class="feed-card-icon">' + (c.icon || "📖") + '</div>'
+ '<div class="feed-card-question">' + esc(c.question || "") + '</div>'
+ '<button class="feed-card-fav' + (isFav ? ' active' : '') + '" onclick="event.stopPropagation();toggleFavorite(\'knowledge\',' + c.id + ',\'' + esc(c.title) + '\',\'' + (c.icon || "📖") + '\')"><span>' + (isFav ? '★' : '☆') + '</span></button>'
+ '</div>'
+ '<div class="feed-card-body"><div class="feed-card-title">' + esc(c.title) + '</div>'
+ '<div class="feed-card-desc">' + esc(c.description) + '</div>'
+ '<div class="feed-card-footer"><span class="feed-card-topic">' + (c.topic ? '🏷 ' + esc(c.topic) : '') + '</span><span class="feed-card-time">' + esc(c.read_time) + '</span></div></div></div>';
}).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 = '<div class="loading" style="padding:40px"><div class="spinner"></div></div>';
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 = '<div class="empty-state"><div class="icon">☆</div><h3>还没有收藏</h3><p>去发现有趣的内容吧</p></div>';
} else {
panel.innerHTML = items.map(function (f) {
return '<div class="history-item">'
+ '<div class="hi-icon" style="background:var(--accent-light)">' + (f.cover || "◎") + '</div>'
+ '<div class="hi-info"><div class="hi-title">' + esc(f.title) + '</div><div class="hi-meta">' + f.target_type + ' · ' + (f.created_at || "").substring(0, 10) + '</div></div>'
+ '<button class="hi-del" onclick="event.stopPropagation();removeFavorite(\'' + f.target_type + '\',' + f.target_id + ')">✕</button>'
+ '</div>';
}).join("");
}
} catch (e) {
panel.innerHTML = '<div class="empty-state"><div class="icon">⚠️</div><h3>加载失败</h3></div>';
}
}
function showHistory() {
loadHistoryList();
}
async function loadHistoryList() {
showProfileListPanel("观看记录");
var panel = document.getElementById("profileListContent");
panel.innerHTML = '<div class="loading" style="padding:40px"><div class="spinner"></div></div>';
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 = '<div class="empty-state"><div class="icon">👀</div><h3>还没有观看记录</h3><p>去看看视频和知识卡片吧</p></div>';
} else {
panel.innerHTML = items.map(function (h) {
return '<div class="history-item">'
+ '<div class="hi-icon" style="background:var(--accent-light)">' + (h.cover || "◎") + '</div>'
+ '<div class="hi-info"><div class="hi-title">' + esc(h.title) + '</div><div class="hi-meta">' + h.target_type + ' · ' + (h.watched_at || "").substring(0, 16) + '</div></div>'
+ '</div>';
}).join("");
}
} catch (e) {
panel.innerHTML = '<div class="empty-state"><div class="icon">⚠️</div><h3>加载失败</h3></div>';
}
}
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 = "<span>📖 " + allKnowledgeCards.length + "个概念</span><span>⏱ 阅读约" + (allKnowledgeCards.length * 5) + "分钟</span>";
}
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 = '<div class="empty-state"><div class="icon">📭</div><h3>暂无相关视频</h3><p>后续将由各学院志愿者上传</p></div>';
}
} catch (e) {
hide("loadingArea");
document.getElementById("videoList").innerHTML = '<div class="empty-state"><div class="icon">⚠️</div><h3>加载失败</h3><p>请稍后重试</p></div>';
}
}
function renderVideoList(videos) {
var list = document.getElementById("videoList");
if (!videos || videos.length === 0) { list.innerHTML = '<div class="empty-state"><div class="icon">📭</div><h3>暂无相关视频</h3></div>'; 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 '<div class="video-card" onclick="openVideo(\'' + v.id + '\',\'' + esc(v.title) + '\',\'' + esc(v.description) + '\',\'' + esc(v.video_url) + '\',\'' + esc(v.author) + '\',\'' + esc(v.sub_category || v.discipline) + '\')">'
+ '<div class="vc-cover" style="background:' + coverBg + '"><div class="vc-cover-placeholder">' + icon + '</div><div class="vc-duration">' + dur + ':00</div>'
+ '<button class="vc-fav' + (isFav ? ' active' : '') + '" onclick="event.stopPropagation();toggleFavorite(\'video\',' + v.id + ',\'' + esc(v.title) + '\',\'' + icon + '\')"><span>' + (isFav ? '★' : '☆') + '</span></button>'
+ '</div>'
+ '<div class="vc-content"><div class="vc-title">' + esc(v.title) + '</div>'
+ '<div class="vc-info-row"><span class="vc-author-avatar" style="background:' + avatarColor + '">' + initial + '</span>'
+ '<span class="vc-author-name">' + esc(v.author) + '</span>'
+ '<div class="vc-stat"><span>👁️</span><span>' + ((hashStr(v.title) % 1000) + 100) + '</span></div></div></div></div>';
}).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 = '<div class="empty-state"><h3>剧情加载失败</h3><p>节点未找到</p></div>';
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 += '<div class="vn-header">';
html += '<div class="vn-chapter-badge">第' + ch + '章 · ' + chName + '</div>';
html += '<div class="vn-stats">';
html += '<span class="stat-badge" style="background:#A0B9D8">📐 数理根基: ' + (vnState.stats.shuli || 10) + '</span>';
html += '<span class="stat-badge" style="background:#DAB8B0">⚔️ 侠义声望: ' + (vnState.stats.xiayi || 10) + '</span>';
html += '<span class="stat-badge" style="background:#B8D0C9">🧘 心志韧性: ' + (vnState.stats.xinzhi || 10) + '</span>';
html += '</div>';
html += '<div class="vn-actions">';
html += '<button onclick="openSavePanel()" class="vn-btn">💾 存档</button>';
html += '<button onclick="openGallery()" class="vn-btn">🏆 图鉴</button>';
html += '<button onclick="resetVNSave()" class="vn-btn outline">🔄 重置</button>';
html += '</div></div>';
// 场景背景 + 立绘
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 += '<div class="story-scene" id="sceneBox">';
if (bgUrl) html += '<img src="' + bgUrl + '" class="vn-background" alt="" onerror="this.style.display=\'none\'" />';
if (spriteUrl) html += '<img src="' + spriteUrl + '" class="vn-sprite" alt="' + esc(characterName) + '" onerror="this.style.display=\'none\'" />';
html += '<div class="story-text-box">';
if (characterName) html += '<div class="story-character">' + esc(characterName) + '</div>';
html += '<div class="story-text" id="sceneText"></div>';
html += '</div></div>';
html += '<div class="story-choices" id="choicesArea"></div>';
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 = '<span class="choice-hint">[' + parts.join(" | ") + ']</span>';
}
btn.innerHTML = '<span class="choice-text">' + esc(c.text) + '</span>' + 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 = '<div class="msg-bubble">' + esc(text) + "</div>";
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 = '<div class="msg-bubble"><div class="typing-indicator"><span></span><span></span><span></span></div></div>';
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 = '<div class="empty-state"><h3>剧情加载失败</h3></div>';
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 += '<div class="sim-header">';
html += '<div class="sim-year-badge" style="background:' + (["", "#3B82F6", "#8B5CF6", "#F97316", "#10B981"][ch]) + '">🎓 大' + ["", "一", "二", "三", "四"][ch] + ' · ' + stage + '</div>';
html += '<div class="sim-stats">';
Object.keys(simState.stats).forEach(function (k) {
html += '<span class="sim-stat-bar" style="background:' + SIM_STAT_COLORS[k] + '">' + SIM_STAT_EMOJIS[k] + ' ' + SIM_STAT_LABELS[k] + ': ' + simState.stats[k] + '</span>';
});
html += '</div>';
html += '<div class="sim-actions-top">';
html += '<button onclick="openSavePanel()" class="vn-btn">💾 存档</button>';
html += '<button onclick="openGallery()" class="vn-btn">🏆 图鉴</button>';
html += '<button onclick="resetSimulator()" class="vn-btn outline">🔄 重新开始</button>';
html += '<button onclick="setAIContext(\'simulator\',\'数学专业模拟器\');openChat()" class="vn-btn">? 问学长</button>';
html += '</div></div>';
html += '<div class="sim-scene">';
if (node.character) html += '<div class="sim-character">' + esc(node.character) + '</div>';
html += '<div class="sim-text" id="simText"></div>';
html += '</div>';
html += '<div class="sim-choices" id="simChoicesArea"></div>';
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 = '<span class="sim-hint">[' + parts.join(" | ") + ']</span>';
}
if (c.hint) effectsHTML += '<span class="sim-hint-tip">💡 ' + esc(c.hint) + '</span>';
btn.innerHTML = '<span class="sim-choice-text">' + esc(c.text) + '</span>' + 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 = '<div style="display:flex;gap:12px;justify-content:center;flex-wrap:wrap;">';
Object.keys(simState.stats).forEach(function (k) {
statsHtml += '<div style="text-align:center"><div style="font-size:22px;font-weight:800;color:' + SIM_STAT_COLORS[k] + '">' + simState.stats[k] + '</div><div style="font-size:11px;color:#999">' + SIM_STAT_LABELS[k] + '</div></div>';
});
statsHtml += '</div>';
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 '<button class="picker-emoji-btn"' + selected + ' onclick="selectAvatar(\'' + e + '\')">' + e + '</button>';
}).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 '<button class="picker-color-btn" style="background:' + c + '"' + selected + ' onclick="selectAvatarColor(\'' + c + '\')"></button>';
}).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 '<div class="ss-hero-card ' + s.cssClass + '" onclick="enterScript(\'' + s.id + '\')">'
+ '<div class="ss-hero-bg"></div>'
+ '<div class="ss-hero-content">'
+ '<div class="ss-hero-icon">' + s.icon + '</div>'
+ '<div class="ss-hero-name">' + esc(s.name) + '</div>'
+ '<div class="ss-hero-desc">' + esc(s.desc) + '</div>'
+ '<div class="ss-hero-meta"><span>' + s.meta + '</span><span class="ss-hero-status">' + statusLabel + '</span></div>'
+ '</div></div>';
}).join("");
// 渲染即将上线小卡
var comingCards = document.getElementById("ssComingCards");
if (comingCards) {
comingCards.innerHTML = COMING_SOON.map(function (c) {
return '<div class="ss-coming-card">'
+ '<div class="ss-coming-card-bg" style="background:' + c.color + '20">' + c.icon + '</div>'
+ '<div class="ss-coming-card-body">'
+ '<div class="ss-coming-card-name">' + esc(c.name) + '</div>'
+ '<div class="ss-coming-card-desc">' + esc(c.desc) + '</div>'
+ '<span class="ss-coming-badge">敬请期待</span>'
+ '</div></div>';
}).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 += '<div class="save-slot active" onclick="loadFromSlot(\'' + scriptId + '\',' + i + ')">';
html += '<div class="save-slot-icon" style="background:' + (scriptId === "simulator" ? "var(--morandi-pink-light)" : "var(--morandi-green-light)") + '">' + icon + '</div>';
html += '<div class="save-slot-info">';
html += '<div class="save-slot-name">' + esc(name) + '</div>';
html += '<div class="save-slot-meta"><span>' + esc(meta) + '</span><span>' + date + '</span></div>';
html += '</div>';
html += '<div class="save-slot-actions">';
html += '<button class="save-slot-btn" onclick="event.stopPropagation();overwriteSlot(\'' + scriptId + '\',' + i + ')">💾 覆盖</button>';
html += '<button class="save-slot-btn" onclick="event.stopPropagation();deleteSlot(\'' + scriptId + '\',' + i + ')">✕</button>';
html += '</div></div>';
} else {
// 空槽位
html += '<div class="save-slot save-slot-empty" onclick="saveToSlot(\'' + scriptId + '\',' + i + ')">';
html += '<span>+ 空槽位 ' + (i + 1) + ' — 点击保存</span>';
html += '</div>';
}
}
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 '<div class="gallery-card unlocked">'
+ '<div class="gallery-badge">' + e.icon + '</div>'
+ '<div class="gallery-card-name">' + esc(e.name) + '</div>'
+ '<div class="gallery-card-desc">' + esc(e.desc) + '</div>'
+ '</div>';
} else {
return '<div class="gallery-card locked">'
+ '<div class="gallery-card-lock">🔒</div>'
+ '<div class="gallery-card-name">???</div>'
+ '<div class="gallery-card-desc">未解锁</div>'
+ '</div>';
}
}).join("");
}