Archived
Demo
This commit is contained in:
@@ -0,0 +1,447 @@
|
||||
// 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);
|
||||
Reference in New Issue
Block a user