Archived
1510 lines
64 KiB
JavaScript
1510 lines
64 KiB
JavaScript
/* =========================================================
|
||
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("");
|
||
}
|