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.min.js
T
2026-08-08 15:49:06 +08:00

1264 lines
57 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.
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: "🧘" };
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" }
];
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"
};
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; }
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("");
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();
}
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");
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";
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" });
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;
var spriteUrl = "";
var characterName = node.character || "";
if (node.sprite) {
spriteUrl = node.sprite;
} else if (characterName) {
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);
});
}
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");
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);
}
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") {
window.location.href = "/mathbti";
}
}
function backToSquare() {
hide("lifeSubNav");
hide("storyContainer");
hide("simulatorContainer");
show("scriptSquare");
renderScriptSquare();
document.querySelector(".content-area").scrollTo({ top: 0, behavior: "smooth" });
}
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";
saveToSlot(scriptId, 0);
}
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("");
}