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

448 lines
16 KiB
JavaScript
Raw Permalink 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.
// MathBTI 前端逻辑 — 12题 → 16结果 → 4族
let testData = null, answers = [], currentQ = 0, myResult = null;
async function init() {
try {
const r = await fetch('/data/seed_mathbti.json');
testData = await r.json();
renderIntro();
} catch (e) {
document.getElementById('introScreen').innerHTML =
'<div style="text-align:center;padding:60px 20px"><p style="color:#94A3B8;margin-bottom:12px">加载失败,请检查网络</p><button class="btn-start" onclick="init()">重试</button></div>';
}
}
function renderIntro() {
const axesEl = document.getElementById('introAxes');
if (axesEl && testData.axes) {
axesEl.innerHTML = testData.axes.map(a =>
'<div class="intro-axis-item">' +
'<span class="axis-emoji">' + (a.emoji_left || '') + '</span>' +
'<span class="axis-label">' + a.name + '</span>' +
'<span class="axis-emoji">' + (a.emoji_right || '') + '</span>' +
'</div>'
).join('');
}
document.querySelector('.intro-meta').textContent =
'⏱ 约2分钟 · 4选项 · 16种人格';
}
function startQuiz() {
answers = new Array(testData.questions.length).fill(-1);
currentQ = 0;
switchScreen('quizScreen');
renderQuestion();
}
function renderQuestion() {
const q = testData.questions[currentQ];
document.getElementById('quizQuestion').textContent = q.question;
const optEl = document.getElementById('quizOptions');
const sel = answers[currentQ];
optEl.innerHTML = q.options.map((o, i) =>
'<label class="quiz-option' + (sel === i ? ' selected' : '') + '" onclick="selectOption(' + i + ')">' +
'<input type="radio" name="q' + currentQ + '" value="' + i + '"' + (sel === i ? ' checked' : '') + '>' +
'<span class="opt-text">' + o.text + '</span></label>'
).join('');
// Progress
const pct = Math.round((currentQ + 1) / testData.questions.length * 100);
document.getElementById('progressFill').style.width = pct + '%';
// 最后一题时进度条末尾显示葫芦图标
const isLast=currentQ===testData.questions.length-1;
document.getElementById('progressFill').style.setProperty('--is-last',isLast?'1':'0');
document.getElementById('progressText').textContent = (currentQ + 1) + '/' + testData.questions.length;
// Next button
const btn = document.getElementById('btnNext');
btn.disabled = answers[currentQ] === -1;
btn.textContent = currentQ === testData.questions.length - 1 ? '查看结果' : '下一题';
}
function selectOption(idx) {
answers[currentQ] = idx;
document.getElementById('btnNext').disabled = false;
// Update visual
document.querySelectorAll('.quiz-option').forEach((el, i) => {
el.classList.toggle('selected', i === idx);
});
}
async function nextQuestion() {
if (answers[currentQ] === -1) return;
if (currentQ < testData.questions.length - 1) {
currentQ++;
renderQuestion();
return;
}
// Last question → calculate locally
const btn = document.getElementById('btnNext');
btn.disabled = true;
btn.textContent = '计算中…';
try {
myResult = calculateResult(answers);
// 自动获得对应的数学人物卡
const mbtiCode=myResult.binary_code;
let gotNewCard=false;
if(mbtiCode){
let cardState2=JSON.parse(localStorage.getItem('hulu_card_state')||'{}');
if(!cardState2.owned)cardState2.owned=[];
if(!cardState2.drawHistory)cardState2.drawHistory=[];
if(cardState2.owned.indexOf(mbtiCode)===-1){
cardState2.owned.push(mbtiCode);
cardState2.drawHistory.push({code:mbtiCode,date:new Date().toISOString().slice(0,10),method:'mathbti'});
localStorage.setItem('hulu_card_state',JSON.stringify(cardState2));
gotNewCard=true;
}
}
renderResult();
// 卡片领取提示
if(gotNewCard){
const res=myResult.result;
const notice=document.getElementById('cardNotice');
if(notice){notice.style.display='block';document.getElementById('cardNoticeName').textContent=res?res.name:'—'}
showToast('🎴 获得数学人物小卡!前往葫芦数学查看卡册');
}
switchScreen('resultScreen');
} catch (e) {
showToast('提交失败,请重试');
btn.disabled = false;
btn.textContent = currentQ === testData.questions.length - 1 ? '查看结果' : '下一题';
}
}
function calculateResult(answers) {
const axes = testData.scoring.axes; // ["style","purpose","era","social"]
const maxPerAxis = testData.scoring.max_per_axis;
const cutoff = testData.scoring.cutoff;
// 计算每个轴的得分
const axisScores = {};
axes.forEach(axis => {
axisScores[axis] = 0;
});
testData.questions.forEach((q, i) => {
const score = q.options[answers[i]].score;
axisScores[q.axis] += score;
});
// 生成二进制代码 (≤cutoff=0左极, ≥cutoff+1=1右极)
const binaryCode = axes.map(axis => axisScores[axis] <= cutoff ? '0' : '1').join('');
// 获取结果
const result = testData.results[binaryCode];
return {
binary_code: binaryCode,
axis_scores: axisScores,
result: result
};
}
function renderResult() {
const res = myResult.result;
if (!res) return;
// Portrait
const portraitEl = document.getElementById('resultPortrait');
if (res.portrait) {
portraitEl.innerHTML = '<div class="result-portrait" style="background:' + (res.icon_bg || '#F0F0F0') + '"><img src="' + res.portrait + '" alt="" style="width:100%;height:100%;object-fit:cover;border-radius:50%" onerror="this.style.display=\'none\';this.nextElementSibling.style.display=\'flex\'"><span class="result-icon-fallback" style="display:none;font-size:48px">' + (res.icon || 'Σ') + '</span></div>';
} else {
portraitEl.innerHTML = '<div class="result-portrait" style="background:' + (res.icon_bg || '#F0F0F0') + '"><span class="result-icon-fallback" style="font-size:48px">' + (res.icon || 'Σ') + '</span></div>';
}
// Name + Clan
document.getElementById('resultName').innerHTML = res.name + ' <span class="type-code" style="font-size:13px;color:#94A3B8;font-weight:400">' + myResult.binary_code + '</span>';
document.getElementById('resultTagline').textContent = res.tagline || '';
// Clan badge
const clanData = testData.clans[res.clan];
document.getElementById('resultMathematician').innerHTML =
(clanData ? '<span class="clan-badge" style="background:' + (clanData.color || '#888') + '15;color:' + (clanData.color || '#888') + ';border:1px solid ' + (clanData.color || '#888') + '30">' + clanData.emoji + ' ' + clanData.name + ' · ' + res.mathematician + '</span>' : '') +
'<span class="mathematician-name">代表数学家:' + (res.mathematician || '—') + '</span>';
// Keywords
const kwEl = document.getElementById('resultKeywords');
if (res.keywords) {
kwEl.innerHTML = res.keywords.map(k => '<span class="kw-tag">' + k + '</span>').join('');
kwEl.style.display = 'flex';
} else {
kwEl.style.display = 'none';
}
// Description
document.getElementById('resultDesc').textContent = res.description || '';
document.getElementById('resultShort').textContent = res.short_desc || '';
// Bio
document.getElementById('bioTitle').textContent = '关于 ' + (res.mathematician || '这位数学家');
document.getElementById('bioText').textContent = res.bio || '';
document.getElementById('bioFact').textContent = res.bio_fact || '';
// Axes bars
const axesEl = document.getElementById('resultAxes');
if (testData.axes) {
axesEl.innerHTML = testData.axes.map(a => {
const score = myResult.axis_scores[a.id] || 0;
const maxScore = testData.scoring.max_per_axis || 9;
const pct = Math.round(score / maxScore * 100);
return '<div class="axis-bar-row">' +
'<span class="axis-bar-label">' + (a.emoji_left || '') + ' ' + a.left + '</span>' +
'<div class="axis-bar-track"><div class="axis-bar-fill" style="width:' + pct + '%"></div></div>' +
'<span class="axis-bar-label">' + a.right + ' ' + (a.emoji_right || '') + '</span>' +
'</div>';
}).join('');
axesEl.style.display = 'block';
}
// Five-dimension ability card from 16位数学家五维能力表
const s5El = document.getElementById('resultStats5');
if (s5El && res.stats5) {
const labels = ['眼光', '人文', '侦探', '建模', '联结'];
s5El.innerHTML = '<div class="result-stats5-title">五维能力</div>' + labels.map(k => {
const v = res.stats5[k] || 0;
return '<div class="axis-bar-row">' +
'<span class="axis-bar-label">' + k + '</span>' +
'<div class="axis-bar-track"><div class="axis-bar-fill" style="width:' + v + '%"></div></div>' +
'<span class="axis-bar-label">' + v + '</span>' +
'</div>';
}).join('');
s5El.style.display = 'block';
}
// Adopt pet
const petData = JSON.parse(localStorage.getItem('mb_pet') || '{}');
const adoptBtn = document.querySelector('.btn-adopt');
if (petData.type) {
adoptBtn.textContent = '✅ 已领养 · 我的数学精灵';
adoptBtn.classList.add('adopted');
}
// Apply clan color
if (clanData) {
document.getElementById('resultScreen').style.setProperty('--clan-color', clanData.color);
}
}
// ---- Bio Expander ----
function toggleBio() {
const body = document.getElementById('bioBody');
const arrow = document.getElementById('bioArrow');
const expanded = body.style.maxHeight && body.style.maxHeight !== '0px';
if (expanded) {
body.style.maxHeight = '0px';
arrow.textContent = '▾';
} else {
body.style.maxHeight = body.scrollHeight + 'px';
arrow.textContent = '▴';
}
}
// ---- Adopt Pet ----
function adoptPet() {
if (!myResult) return;
const res = myResult.result;
const dims = {};
['眼光', '人文', '侦探', '建模', '联结'].forEach(k => {
dims[k] = Math.max(10, Math.min(100, Number((res.stats5 && res.stats5[k]) || 0)));
});
const pet = {
name: res.name || '—',
type: res.name || '—',
icon: res.icon || 'Σ',
color: res.color || '#5B6AF0',
mathematician: res.mathematician || '',
clan: res.clan || '',
binary_code: myResult.binary_code,
level: 1,
exp: 0,
dims: dims
};
localStorage.setItem('mb_pet', JSON.stringify(pet));
localStorage.setItem('mathbrain_pet', JSON.stringify(pet));
const btn = document.querySelector('.btn-adopt');
btn.textContent = '✅ 已领养 · 我的数学精灵';
btn.classList.add('adopted');
showToast('🧬 数学精灵已领养!每天来葫芦数学 App 喂它成长');
}
// ---- Share ----
function copyShare() {
if (!myResult) return;
const res = myResult.result;
const clanData = testData.clans[res.clan];
const text = '我的数学人格是「' + res.name + '」' + (res.icon || '') + '\n' +
(clanData ? clanData.emoji + ' ' + clanData.name + ' · ' + res.mathematician : '') + '\n' +
(res.short_desc || '') + '\n\n' +
'👉 你也来测测:' + window.location.href + '\n\n' +
'#数学人格测试 #MathBTI';
navigator.clipboard.writeText(text).then(() => showToast('已复制分享文案!'));
}
function shareResult() {
if (!myResult) return;
const res = myResult.result;
const text = '我的数学人格是「' + res.name + '」' + (res.icon || '') + '' + (res.short_desc || '');
if (navigator.share) {
navigator.share({ title: 'MathBTI · 数学人格测试', text: text, url: window.location.href }).catch(() => {});
} else {
copyShare();
}
}
function generateShareCard() {
if (!myResult) return;
const res = myResult.result;
const clanData = testData.clans[res.clan];
const canvas = document.getElementById('shareCardCanvas');
const ctx = canvas.getContext('2d');
canvas.width = 600;
canvas.height = 800;
// Background
const gradient = ctx.createLinearGradient(0, 0, 600, 800);
gradient.addColorStop(0, (res.color || '#5B6AF0') + '22');
gradient.addColorStop(1, (res.color || '#5B6AF0') + '08');
ctx.fillStyle = '#FAFBFC';
ctx.fillRect(0, 0, 600, 800);
ctx.fillStyle = gradient;
ctx.fillRect(0, 0, 600, 800);
// Top accent bar
ctx.fillStyle = res.color || '#5B6AF0';
ctx.fillRect(0, 0, 600, 6);
// Portrait placeholder
ctx.fillStyle = res.icon_bg || '#F0F0F0';
ctx.beginPath();
ctx.arc(300, 180, 70, 0, Math.PI * 2);
ctx.fill();
ctx.fillStyle = '#334155';
ctx.font = '60px sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(res.icon || 'Σ', 300, 180);
// Name
ctx.fillStyle = '#1E293B';
ctx.font = 'bold 36px "PingFang SC", sans-serif';
ctx.fillText(res.name, 300, 280);
// Clan + Mathematician
ctx.fillStyle = '#64748B';
ctx.font = '18px "PingFang SC", sans-serif';
if (clanData) {
ctx.fillText(clanData.emoji + ' ' + clanData.name + ' · ' + res.mathematician, 300, 315);
}
// Tagline
ctx.fillStyle = res.color || '#5B6AF0';
ctx.font = 'italic 15px "PingFang SC", sans-serif';
const tagline = res.tagline || '';
wrapText(ctx, tagline, 300, 350, 480, 22);
// Separator
ctx.fillStyle = '#E2E8F0';
ctx.fillRect(60, 380, 480, 1);
// Short desc
ctx.fillStyle = '#334155';
ctx.font = '15px "PingFang SC", sans-serif';
const desc = res.short_desc || '';
wrapText(ctx, desc, 300, 410, 480, 24);
// Axes
let ay = 450;
if (testData.axes && myResult.axis_scores) {
ctx.fillStyle = '#64748B';
ctx.font = '14px "PingFang SC", sans-serif';
testData.axes.forEach(a => {
const s = myResult.axis_scores[a.id] || 0;
const maxS = testData.scoring.max_per_axis || 9;
const pct = Math.round(s / maxS * 100);
ctx.fillText(a.emoji_left + ' ' + a.left + ' — ' + pct + '% — ' + a.right + ' ' + a.emoji_right, 300, ay);
ay += 28;
});
}
// Bottom
ctx.fillStyle = '#94A3B8';
ctx.font = '13px "PingFang SC", sans-serif';
ctx.fillText('葫芦数学 · MathBTI', 300, 730);
// 葫芦水印
ctx.save();ctx.globalAlpha=.06;ctx.font='80px sans-serif';ctx.fillStyle='#A8907E';
ctx.fillText('🏮',480,680);
ctx.restore();
ctx.fillText('你也来测测你的数学人格类型 →', 300, 755);
// QR placeholder
ctx.fillStyle = '#E2E8F0';
ctx.fillRect(255, 640, 90, 90);
ctx.fillStyle = '#94A3B8';
ctx.font = '11px sans-serif';
ctx.fillText('扫码测', 300, 685);
document.getElementById('shareCardPreview').style.display = 'block';
document.getElementById('shareCardPreview').scrollIntoView({ behavior: 'smooth' });
}
function downloadShareCard() {
const canvas = document.getElementById('shareCardCanvas');
const link = document.createElement('a');
link.download = 'mathbti_' + (myResult ? myResult.binary_code : 'result') + '.png';
link.href = canvas.toDataURL('image/png');
link.click();
showToast('📸 图片已保存!');
}
function wrapText(ctx, text, x, y, maxWidth, lineHeight) {
const lines = [];
let current = '';
for (let i = 0; i < text.length; i++) {
const test = current + text[i];
if (ctx.measureText(test).width > maxWidth && current.length > 0) {
lines.push(current);
current = text[i];
} else {
current = test;
}
}
if (current) lines.push(current);
lines.forEach((line, idx) => {
ctx.fillText(line, x, y + idx * lineHeight);
});
}
// ---- Retry ----
function retryQuiz() {
answers = [];
currentQ = 0;
myResult = null;
document.getElementById('bioBody').style.maxHeight = '0px';
document.getElementById('bioArrow').textContent = '▾';
document.getElementById('shareCardPreview').style.display = 'none';
switchScreen('introScreen');
}
// ---- Helpers ----
function switchScreen(id) {
document.querySelectorAll('.screen').forEach(s => s.classList.remove('active'));
document.getElementById(id).classList.add('active');
window.scrollTo({ top: 0, behavior: 'smooth' });
}
let toastTimer = null;
function showToast(msg) {
const el = document.getElementById('toast');
el.textContent = msg;
el.classList.add('show');
clearTimeout(toastTimer);
toastTimer = setTimeout(() => el.classList.remove('show'), 2200);
}
// Bootstrap
document.addEventListener('DOMContentLoaded', init);