Merge pull request 'update bug1.0' (#13) from bug1.0 into main

Reviewed-on: http://117.72.28.96:8765/Jacky/Hulumath-Web/pulls/13
This commit was merged in pull request #13.
This commit is contained in:
2026-08-09 03:34:04 +08:00
23 changed files with 313 additions and 17 deletions
@@ -134,7 +134,7 @@ class Command(BaseCommand):
def handle(self, *args, **options):
docs = Path(settings.PROJECT_ROOT) / "docs"
mathbti_path = docs / "old_scripts" / "seed_mathbti.json"
story_path = docs / "数学少年线_story.json"
story_path = docs / "信仰者线_story.json"
videos_path = docs / "old_scripts" / "seed_videos.json"
if not mathbti_path.exists() or not story_path.exists() or not videos_path.exists():
raise CommandError("缺少 docs 中的初始内容文件")
@@ -167,7 +167,7 @@ class Command(BaseCommand):
if errors:
raise CommandError("; ".join(errors))
flagship, _ = Story.objects.update_or_create(
slug="believer-math-teen",
slug="believer",
defaults={
"title": story_document["title"],
"summary": story_document.get("description", ""),
+73
View File
@@ -274,3 +274,76 @@ dialog::backdrop { background: rgba(17,25,20,.55); backdrop-filter: blur(5px); }
.map-stage { height: 480px; transform: scale(.92); }.ability-node { width: 108px; height: 94px; }.node-vision { left: calc(50% - 54px); }.node-humanities { left: 0; top: 100px; }.node-connection { right: 0; top: 100px; }.node-detection { left: 2%; bottom: 30px; }.node-modeling { right: 2%; bottom: 30px; }.pet-node { top: 190px; }
.map-lines { display: none; }.ability-legend { margin-top: 14px; justify-content: start; }.video-cover { height: 150px; }
}
.life-hub, .life-skills { animation: rise .35s ease; }
.back-button { display: inline-flex; align-items: center; gap: 6px; border: 1px solid var(--line); border-radius: 10px; padding: 8px 16px; background: transparent; color: var(--muted); cursor: pointer; margin-bottom: 18px; font-size: 13px; transition: .2s ease; }
.back-button:hover { color: var(--ink); border-color: var(--ink); }
.life-map { margin-top: 28px; }
.life-map-canvas { position: relative; max-width: 1200px; margin: 0 auto; display: grid; grid-template-columns: 1fr 1fr; gap: 40px; padding: 30px 0; }
.life-map-lines { position: absolute; inset: 0; width: 100%; height: 100%; z-index: 0; pointer-events: none; }
.life-map-center { position: absolute; left: 50%; top: 50%; transform: translate(-50%, -50%); z-index: 1; display: grid; place-items: center; text-align: center; pointer-events: none; }
.life-map-center b { display: grid; place-items: center; width: 56px; height: 56px; border-radius: 50%; background: var(--ink); color: var(--lime); font: 22px Georgia, serif; margin-bottom: 6px; }
.life-map-center span { font-size: 9px; color: var(--muted); letter-spacing: .12em; line-height: 1.4; }
.route-card {
position: relative; width: 100%; padding: 28px 26px 24px; border-radius: 22px; z-index: 2;
border: 1px solid rgba(23,33,27,.1); background: rgba(255,255,252,.82); backdrop-filter: blur(14px);
box-shadow: 0 24px 70px rgba(38,48,40,.06); cursor: pointer; transition: .25s ease; display: flex; flex-direction: column; min-height: 300px;
}
.route-card:hover { transform: translateY(-6px); box-shadow: 0 28px 80px rgba(38,48,40,.1); }
.route-card::after { content: ""; position: absolute; width: 80px; height: 80px; border-radius: 50%; right: -28px; top: -28px; opacity: .5; }
.route-believer::after { background: var(--lime); }
.route-spreader::after { background: var(--blue); }
.route-applier::after { background: var(--orange); }
.route-seer::after { background: #bb8ac4; }
.route-index { font: 700 13px Georgia, serif; color: #b0b6ae; letter-spacing: .08em; }
.route-spirit { margin-top: 14px; } .route-spirit b { display: block; font: 25px Georgia, serif; } .route-spirit small { font-size: 9px; color: var(--muted); letter-spacing: .22em; }
.route-conflict { margin: 16px 0 18px; color: #3e4942; font-size: 14px; line-height: 1.7; }
.route-mathematicians { display: flex; gap: 10px; margin-bottom: 18px; }
.route-mathematician { display: flex; flex-direction: column; align-items: center; gap: 5px; flex: 1; }
.route-mathematician img { width: 52px; height: 52px; border-radius: 50%; object-fit: cover; border: 2px solid rgba(23,33,27,.08); }
.route-mathematician b { font-size: 10px; text-align: center; }
.route-mathematician small { font-size: 8px; color: var(--muted); text-align: center; line-height: 1.3; }
.route-meta { margin-top: auto; margin-bottom: 16px; }
.route-status { font-size: 10px; color: var(--green); font-weight: 700; letter-spacing: .06em; }
.route-enter { border: 0; border-bottom: 2px solid currentColor; padding: 4px 0; background: transparent; cursor: pointer; font-size: 14px; font-weight: 650; text-align: left; }
.route-believer .route-enter { color: var(--green); } .route-spreader .route-enter { color: var(--blue); }
.route-applier .route-enter { color: var(--orange); } .route-seer .route-enter { color: #9b6bb0; }
.alumni-entry { margin-top: 50px; border-radius: 22px; background: var(--ink); color: white; padding: 40px 46px; overflow: hidden; position: relative; }
.alumni-entry::after { content: ""; position: absolute; width: 280px; height: 280px; right: -80px; bottom: -120px; border: 45px solid rgba(204,232,91,.08); border-radius: 50%; }
.alumni-entry-content { display: flex; align-items: center; justify-content: space-between; gap: 30px; position: relative; z-index: 1; }
.alumni-entry .kicker { color: var(--lime); }
.alumni-entry h2 { margin: 12px 0 10px; font: 30px Georgia, serif; }
.alumni-entry p { color: #aeb9b1; font-size: 14px; line-height: 1.7; max-width: 480px; }
.alumni-entry-action { display: flex; flex-direction: column; align-items: flex-end; gap: 14px; }
.alumni-entry-action span { font-size: 11px; color: #aeb9b1; }
.alumni-entry .primary-button { background: var(--lime); color: var(--ink); }
.skill-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 18px; }
.skill-grid.loading { display: block; color: var(--muted); padding: 40px 0; }
.skill-card { border: 1px solid var(--line); border-radius: 18px; background: rgba(255,255,252,.82); overflow: hidden; transition: .2s ease; }
.skill-card:hover { transform: translateY(-4px); box-shadow: 0 18px 38px rgba(38,48,40,.1); }
.skill-header { height: 90px; background: var(--ink); display: flex; align-items: flex-end; padding: 18px; }
.skill-meta { color: var(--lime); font-size: 10px; font-weight: 700; letter-spacing: .16em; }
.skill-body { padding: 22px; display: flex; flex-direction: column; min-height: 180px; }
.skill-body h3 { margin: 0 0 10px; font: 21px/1.3 Georgia, serif; }
.skill-body p { color: var(--muted); font-size: 13px; line-height: 1.7; flex: 1; }
.skill-body footer { display: flex; align-items: center; justify-content: space-between; margin-top: 18px; }
.skill-body footer span { color: var(--muted); font-size: 12px; }
.skill-body footer button { border: 0; border-bottom: 2px solid var(--green); background: transparent; color: var(--green); padding: 4px 0; cursor: pointer; font-weight: 650; }
.skill-body footer button:disabled { cursor: not-allowed; opacity: .5; }
@media (max-width: 1050px) {
.life-map-canvas { grid-template-columns: 1fr; }
.life-map-lines, .life-map-center { display: none; }
.alumni-entry-content { flex-direction: column; align-items: flex-start; }
.alumni-entry-action { align-items: flex-start; }
.skill-grid { grid-template-columns: 1fr 1fr; }
}
@media (max-width: 700px) {
.skill-grid { grid-template-columns: 1fr; }
.alumni-entry { padding: 28px 24px; }
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 70 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 72 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 64 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 74 KiB

+150 -13
View File
@@ -1,3 +1,30 @@
const ROUTE_MATHEMATICIANS = {
believer: [
{ name: "高斯", portrait: "/static/img/mathematicians/1000.png", identity: "定理工匠" },
{ name: "祖冲之", portrait: "/static/img/mathematicians/1010.png", identity: "穷竭守尺人" },
{ name: "欧拉", portrait: "/static/img/mathematicians/1001.png", identity: "公式狂僧" },
{ name: "诺特", portrait: "/static/img/mathematicians/1011.png", identity: "抽象代数宗师" },
],
spreader: [
{ name: "斐波那契", portrait: "/static/img/mathematicians/1100.png", identity: "商路算郎" },
{ name: "华罗庚", portrait: "/static/img/mathematicians/1110.png", identity: "双法布道者" },
{ name: "伽罗瓦", portrait: "/static/img/mathematicians/1101.png", identity: "决斗狂热者" },
{ name: "Lovelace", portrait: "/static/img/mathematicians/1111.png", identity: "织机先知" },
],
applier: [
{ name: "秦九韶", portrait: "/static/img/mathematicians/0100.png", identity: "大衍谋士" },
{ name: "牛顿", portrait: "/static/img/mathematicians/0110.png", identity: "宇宙立法者" },
{ name: "图灵", portrait: "/static/img/mathematicians/0111.png", identity: "密码破译使" },
{ name: "冯·诺依曼", portrait: "/static/img/mathematicians/0101.png", identity: "博弈游侠" },
],
seer: [
{ name: "希帕提娅", portrait: "/static/img/mathematicians/0000.png", identity: "几何殉道者" },
{ name: "赵爽", portrait: "/static/img/mathematicians/0010.png", identity: "弦图先觉" },
{ name: "庞加莱", portrait: "/static/img/mathematicians/0001.png", identity: "拓扑游方" },
{ name: "约翰逊", portrait: "/static/img/mathematicians/0011.png", identity: "人脑计算机" },
],
};
const state = {
user: null,
stories: [],
@@ -135,25 +162,100 @@ function card({ meta, title, body, foot, action, onClick, disabled = false }) {
return article;
}
function renderRouteMathematicians() {
Object.entries(ROUTE_MATHEMATICIANS).forEach(([clan, mathematicians]) => {
const root = $(`[data-mathematicians="${clan}"]`);
if (!root) return;
root.replaceChildren(...mathematicians.map((person) => {
const item = document.createElement("div");
item.className = "route-mathematician";
const img = document.createElement("img");
img.src = person.portrait;
img.alt = person.name;
img.loading = "lazy";
const name = document.createElement("b");
name.textContent = person.name;
const identity = document.createElement("small");
identity.textContent = person.identity;
item.append(img, name, identity);
return item;
}));
});
}
async function loadStories() {
const root = $("#story-list");
try {
state.stories = await api("math-life/stories/");
root.classList.remove("loading");
root.replaceChildren(...state.stories.map((story) => card({
meta: story.kind === "flagship" ? "旗舰人生" : story.kind === "skill" ? "人物 Skill" : "特别篇",
title: story.title,
body: story.summary || "在不完整信息中判断机会、关系与代价。",
foot: `${story.estimated_minutes} 分钟`,
action: story.available ? "开始人生 →" : "查看预告",
disabled: !story.available,
onClick: () => beginStory(story.slug),
})));
renderRouteBadges();
renderSkillList();
} catch (error) {
root.textContent = error.message;
showToast(error.message);
}
}
function renderRouteBadges() {
const flagship = state.stories.find((story) => story.kind === "flagship" && story.available);
const skillCount = state.stories.filter((story) => story.kind === "skill").length;
const alumniCount = $("#alumni-count");
alumniCount.textContent = skillCount > 0 ? `当前 ${skillCount} 个校友人生可体验` : "即将上线校友访谈";
if (flagship) {
const route = flagship.slug.includes("believer") ? "believer" : "believer";
const card = $(`.route-card[data-route="${route}"]`);
if (card) {
card.querySelector(".route-status").textContent = `可体验 · 约 ${flagship.estimated_minutes} 分钟`;
}
}
}
function renderSkillList() {
const root = $("#skill-list");
const skills = state.stories.filter((story) => story.kind === "skill");
if (skills.length === 0) {
root.classList.remove("loading");
root.textContent = "校友访谈即将上线,敬请期待。";
return;
}
root.classList.remove("loading");
root.replaceChildren(...skills.map((story) => {
const article = document.createElement("article");
article.className = "skill-card";
const header = document.createElement("div");
header.className = "skill-header";
const meta = document.createElement("span");
meta.className = "skill-meta";
meta.textContent = story.kind === "skill" ? "校友访谈" : "特别篇";
header.append(meta);
const body = document.createElement("div");
body.className = "skill-body";
const title = document.createElement("h3");
title.textContent = story.title;
const summary = document.createElement("p");
summary.textContent = story.summary || "在不完整信息中判断机会、关系与代价。";
const footer = document.createElement("footer");
const duration = document.createElement("span");
duration.textContent = `${story.estimated_minutes} 分钟`;
const button = document.createElement("button");
button.textContent = story.available ? "开始人生 →" : "查看预告";
button.disabled = !story.available;
if (story.available) button.addEventListener("click", () => beginStory(story.slug));
footer.append(duration, button);
body.append(title, summary, footer);
article.append(header, body);
return article;
}));
}
function showLifeHub() {
$("#life-hub").classList.remove("hidden");
$("#life-skills").classList.add("hidden");
}
function showLifeSkills() {
$("#life-hub").classList.add("hidden");
$("#life-skills").classList.remove("hidden");
window.scrollTo({ top: 0, behavior: "smooth" });
}
async function beginStory(slug) {
if (!requireAuth()) return;
try {
@@ -909,6 +1011,18 @@ function switchTool(tool) {
window.HuluToolbox?.activate(tool);
}
function renderLatexPreview(source) {
const target = $("#latex-preview");
target.replaceChildren();
if (!source.trim()) return;
if (typeof katex === "undefined") { target.textContent = source; return; }
try {
katex.render(source, target, { throwOnError: false, displayMode: true });
} catch {
target.textContent = source;
}
}
async function saveFormula() {
if (!requireAuth()) return;
try {
@@ -928,13 +1042,34 @@ function bindUI() {
$$("[data-open-auth]").forEach((button) => button.addEventListener("click", openAuth));
$("#start-mathbti").addEventListener("click", startMathBTI);
$("#save-formula").addEventListener("click", saveFormula);
$("#latex-source").addEventListener("input", (event) => { $("#latex-preview").textContent = event.target.value; });
$("#latex-source").addEventListener("input", (event) => { renderLatexPreview(event.target.value); });
$$(".tool-card").forEach((button) => {
button.addEventListener("click", () => switchTool(button.dataset.tool));
});
$("#symbol-search").addEventListener("input", (event) => {
renderSymbols(event.target.value);
});
$("#enter-alumni").addEventListener("click", showLifeSkills);
$("#back-to-hub").addEventListener("click", showLifeHub);
$$(".route-card").forEach((card) => {
card.addEventListener("click", (event) => {
if (event.target.closest(".route-enter")) return;
const route = card.dataset.route;
const flagship = state.stories.find(
(story) => story.kind === "flagship" && story.available && story.slug.includes(route),
);
if (flagship) beginStory(flagship.slug);
else showToast(`${card.querySelector(".route-spirit b").textContent}人生即将开放`);
});
card.querySelector(".route-enter").addEventListener("click", () => {
const route = card.dataset.route;
const flagship = state.stories.find(
(story) => story.kind === "flagship" && story.available && story.slug.includes(route),
);
if (flagship) beginStory(flagship.slug);
else showToast(`${card.querySelector(".route-spirit b").textContent}人生即将开放`);
});
});
$$(".ability-node").forEach((button) => {
button.addEventListener("click", () => {
state.videoAbility =
@@ -992,6 +1127,7 @@ function bindUI() {
async function boot() {
bindUI();
renderRouteMathematicians();
window.HuluToolbox?.init();
window.HuluRealtime?.init();
renderSymbols();
@@ -1002,6 +1138,7 @@ async function boot() {
loadContent(),
window.HuluGames?.load(),
]);
renderLatexPreview($("#latex-source").value);
}
boot();
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+86 -2
View File
@@ -7,6 +7,7 @@
<meta name="description" content="葫芦数学:发现你的数学人格,经历属于你的数学人生。">
<title>葫芦数学 · 数学人生宇宙</title>
<link rel="stylesheet" href="{% static 'css/app.css' %}">
<link rel="stylesheet" href="{% static 'vendor/katex/katex.min.css' %}">
</head>
<body>
<div class="ambient ambient-one"></div>
@@ -80,8 +81,90 @@
</section>
<section class="view" id="view-life">
<div class="page-title"><span class="kicker">MATH LIFE HALL</span><h1>数学人生大厅</h1><p>长篇旗舰人生、人物 Skill 与特别篇,共用同一份成长档案。</p></div>
<div id="story-list" class="content-grid loading">正在读取已发布人生…</div>
<div class="life-hub" id="life-hub">
<div class="page-title"><span class="kicker">MATH LIFE HALL</span><h1>数学人生大厅</h1><p>四条旗舰人生对应四种数学精神,人物 Skill 包持续扩展真实人生样本。</p></div>
<div class="life-map" id="life-map">
<div class="life-map-canvas">
<svg class="life-map-lines" viewBox="0 0 1200 520" preserveAspectRatio="xMidYMid meet" aria-hidden="true">
<path d="M600 260 Q400 120 220 180" fill="none" stroke="rgba(25,101,72,.12)" stroke-width="2" stroke-dasharray="10 12"/>
<path d="M600 260 Q800 120 980 180" fill="none" stroke="rgba(25,101,72,.12)" stroke-width="2" stroke-dasharray="10 12"/>
<path d="M600 260 Q400 400 220 420" fill="none" stroke="rgba(25,101,72,.12)" stroke-width="2" stroke-dasharray="10 12"/>
<path d="M600 260 Q800 400 980 420" fill="none" stroke="rgba(25,101,72,.12)" stroke-width="2" stroke-dasharray="10 12"/>
<circle cx="600" cy="260" r="90" fill="rgba(25,101,72,.04)" stroke="rgba(25,101,72,.16)" stroke-width="2"/>
<circle cx="600" cy="260" r="70" fill="none" stroke="rgba(25,101,72,.12)" stroke-width="2" stroke-dasharray="6 8"/>
</svg>
<div class="life-map-center">
<b></b><span>数学人生<br>交叉点</span>
</div>
<article class="route-card route-believer" data-clan="believer" data-route="believer">
<span class="route-index">01</span>
<div class="route-spirit">
<b>信仰者</b><small>BELIEVER</small>
</div>
<p class="route-conflict">当答案还不存在,你是否仍愿意相信推理?</p>
<div class="route-mathematicians" data-mathematicians="believer"></div>
<div class="route-meta"><span class="route-status">旗舰人生 · 约 120 分钟</span></div>
<button class="route-enter" data-route-enter="believer">进入信仰者人生 →</button>
</article>
<article class="route-card route-spreader" data-clan="spreader" data-route="spreader">
<span class="route-index">02</span>
<div class="route-spirit">
<b>传播者</b><small>SPREADER</small>
</div>
<p class="route-conflict">你会守住发现,还是让更多人理解它?</p>
<div class="route-mathematicians" data-mathematicians="spreader"></div>
<div class="route-meta"><span class="route-status">即将开放 · 序章可试</span></div>
<button class="route-enter" data-route-enter="spreader">进入传播者人生 →</button>
</article>
<article class="route-card route-applier" data-clan="applier" data-route="applier">
<span class="route-index">03</span>
<div class="route-spirit">
<b>应用者</b><small>APPLIER</small>
</div>
<p class="route-conflict">数字进入现实之后,谁来承担模型的代价?</p>
<div class="route-mathematicians" data-mathematicians="applier"></div>
<div class="route-meta"><span class="route-status">即将开放 · 序章可试</span></div>
<button class="route-enter" data-route-enter="applier">进入应用者人生 →</button>
</article>
<article class="route-card route-seer" data-clan="seer" data-route="seer">
<span class="route-index">04</span>
<div class="route-spirit">
<b>直觉者</b><small>SEER</small>
</div>
<p class="route-conflict">当形式尚未成形,你敢不敢追随结构感?</p>
<div class="route-mathematicians" data-mathematicians="seer"></div>
<div class="route-meta"><span class="route-status">即将开放 · 序章可试</span></div>
<button class="route-enter" data-route-enter="seer">进入直觉者人生 →</button>
</article>
</div>
</div>
<div class="alumni-entry" id="alumni-entry">
<div class="alumni-entry-content">
<div>
<span class="kicker">ALUMNI INTERVIEW</span>
<h2>数学校友访谈</h2>
<p>我们采访真实的数学校友,把他们的关键人生抉择浓缩为 20 分钟可体验的数学人生。</p>
</div>
<div class="alumni-entry-action">
<span id="alumni-count">读取中…</span>
<button class="primary-button" id="enter-alumni">进入访谈大厅 <b></b></button>
</div>
</div>
</div>
</div>
<div class="life-skills hidden" id="life-skills">
<div class="page-title">
<button class="back-button" id="back-to-hub">← 返回数学人生大厅</button>
<span class="kicker">ALUMNI INTERVIEW</span><h1>数学校友访谈</h1><p>每位校友的数学人生,浓缩为约 20 分钟的关键抉择体验。</p>
</div>
<div id="skill-list" class="skill-grid loading">正在读取校友人生…</div>
</div>
</section>
<section class="view" id="view-contest">
@@ -317,6 +400,7 @@
<div class="toast" id="toast" role="status"></div>
<div class="csrf-token">{% csrf_token %}</div>
<script src="{% static 'vendor/katex/katex.min.js' %}" defer></script>
<script src="{% static 'js/toolbox.js' %}" defer></script>
<script src="{% static 'js/games.js' %}" defer></script>
<script src="{% static 'js/realtime.js' %}" defer></script>