Files
huiyuti/js/app.js
2026-04-26 13:47:21 +08:00

685 lines
26 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.
/**
* 传承基因测试 - 主逻辑
* 包含:题目数据、类型数据、答题流程、计分逻辑、雷达图绘制、海报生成
*/
(function () {
'use strict';
// ========================================
// 题目数据18题6个维度每维度3题
// ========================================
const questions = [
// 维度1 - 自我觉察力题1-3
'当我感到烦躁时,我能说出自己真正在气什么,而不是只觉得"烦"',
'我经常反思自己的行为模式,而不是只看到别人的问题',
'面对冲突时,我能觉察到自己身体的变化(心跳加速、肌肉紧绷等)',
// 维度2 - 情绪驾驭力题4-6
'在和孩子吵架后,我能让自己冷静下来再去沟通,而不是冷战或翻旧账',
'当情绪上来时,我能让自己暂停几秒再回应,而不是脱口而出',
'我很少因为一时冲动说出让自己后悔的话',
// 维度3 - 关系修复力题7-9
'我发现自己和父母的相处方式,正在不知不觉中复制到我和孩子的关系上',
'当关系出现问题时,我愿意主动去修复,而不是等对方先改变',
'我能分辨哪些情绪是自己的,哪些是"替别人承担的"',
// 维度4 - 传承意识题10-12
'我想过自己的某些性格特点或反应模式,可能是从祖辈那里继承来的',
'我注意到家族里有些模式在一代代重复(比如某种情绪、某种关系模式)',
'我相信了解家族的历史和故事,对理解和教育孩子有帮助',
// 维度5 - 内在驱动力题13-15
'我清楚自己为什么要做现在做的事,而不是随大流或被外界推着走',
'我觉得自己的人生是自己主动选择的,而不是被安排好的',
'遇到困难时,我能找到继续前行的内在力量,而不是只靠外部压力',
// 维度6 - 成长信念题16-18
'我相信不管现在是什么状态,人都可以改变和成长',
'我愿意为了长远的目标,每天坚持做一点小事',
'我觉得"慢慢来"不是放弃,而是一种智慧'
];
// 维度名称(用于雷达图标签)
const dimensionNames = [
'自我觉察力', '情绪驾驭力', '关系修复力',
'传承意识', '内在驱动力', '成长信念'
];
// ========================================
// 8种类型数据
// ========================================
const types = {
awakener: {
name: '觉醒者',
icon: 'assets/icons/awakener.png',
desc: '已走在自我成长路上,觉察力和情绪力都很强',
quote: '「看见即觉察,觉察即涨心」',
analysis: '你拥有出色的自我觉察能力,能够清晰地看见自己的情绪和念头。在情绪管理方面,你已经掌握了"知止"的智慧,能在关键时刻停住并选择回应方式。你正在成为自己生活的"第一目击证人",内在诚信正在建立。继续深化修炼,从"看见"走向"止于至善",你将影响整个家族的传承模式。',
recommend: '你已经走在觉醒的路上,课程将帮你从"看见"走向"止于至善",深化修炼,让美好状态持续。'
},
icebreaker: {
name: '破冰人',
icon: 'assets/icons/icebreaker.png',
desc: '正在打破家族旧模式,传承意识和关系修复力突出',
quote: '「旧地图无法发现新大陆」',
analysis: '你对家族传承有着深刻的意识,能够看到代际之间传递的模式。你已经开始打破那些不再服务于你的共生剧本,在关系修复方面展现出勇气和行动力。你正在成为家族中"那个不一样的人"。系统学习冰山模型,将帮助你更深入地理解这些模式背后的根源。',
recommend: '你已开始打破旧模式,课程将带你系统理解冰山下的家族数据,找到真正的破冰点。'
},
pauser: {
name: '知止行者',
icon: 'assets/icons/pauser.png',
desc: '情绪驾驭力突出,能在关键时刻停住',
quote: '「在电光火石间看见自己」',
analysis: '你拥有强大的情绪驾驭能力能够在情绪升起的瞬间停住善用那0.0001秒的间隙。这是非常珍贵的能力——很多人终其一生都在被情绪带着走。不过,你还可以更进一步:从"知止"到"看见",觉察情绪背后的根源。当你能看见情绪从哪里来,你就真正掌握了选择的自由。',
recommend: '你已能在情绪中停住,课程将帮你看见情绪背后的根源,从"知止"走向"格物致知"。'
},
guardian: {
name: '传承守护者',
icon: 'assets/icons/guardian.png',
desc: '传承意识强、成长信念坚定,但行动力还不足',
quote: '「冰冻三尺非一日之寒,精诚所至金石为开」',
analysis: '你对家族传承有着深刻的理解和坚定的成长信念,你知道改变需要时间和耐心。你的内在已经有了清晰的认知,但从意识到行动之间还有一段距离。这不是你的问题——冰冻三尺非一日之寒。关键是从最小的行动开始,日拱一卒,让认知慢慢落地为每天的功课。',
recommend: '你对传承有深刻意识,课程将帮你把意识转化为每天的功课,从"知道"到"做到"。'
},
seeker: {
name: '寻道者',
icon: 'assets/icons/seeker.png',
desc: '成长信念强、内在驱动力高,但方向还不够清晰',
quote: '「通过外面生活现象,找到内在卡点」',
analysis: '你拥有强大的内在驱动力和坚定的成长信念,这是非常宝贵的品质。你已经准备好改变,也愿意为此投入时间和精力。但有时候你可能感到方向不够清晰——知道要改变,但不知道从哪里开始。这正是"格物致知"的用武之地:通过外面生活现象找到内在卡点,方向自然就会浮现。',
recommend: '你有强大的成长动力,课程将帮你找到内在卡点,明确方向,从"寻找"到"看见"。'
},
sleeper: {
name: '沉睡者',
icon: 'assets/icons/sleeper.png',
desc: '多维度偏低,但成长信念还在,是觉醒的起点',
quote: '「每个人本自具足,生来就足够好」',
analysis: '你可能觉得自己在生活的惯性中运转,很多方面都还没有找到方向。但请看到一件重要的事:你的成长信念还在——你相信人可以改变,这本身就是觉醒的种子。很多人连这颗种子都没有。觉醒的第一步,不是做更多,而是"看见"——看见自己的情绪、念头和模式。从"我看见我"开始,一切都会不同。',
recommend: '觉醒的第一步是看见。课程将从最基础的"我看见我"开始,帮你唤醒内在的力量。'
},
relational: {
name: '关系困者',
icon: 'assets/icons/relational.png',
desc: '关系是最大痛点,情绪驾驭和关系修复都较弱',
quote: '「找回足够好的自己,用它修复生活」',
analysis: '关系是你当前最大的挑战和突破口。你可能发现自己总是在重复某些关系模式——和父母的、和伴侣的、和孩子的。这些模式不是你的错,它们是家族大数据通过代际传承传递下来的"共生剧本"。好消息是,你已经意识到了问题所在,这就是改变的开始。从"我看见我的妈妈"开始,你将慢慢解开这些关系的结。',
recommend: '关系是你的突破口,课程将教你从"我看见我的妈妈"开始,修复关系,走出共生剧本。'
},
habitual: {
name: '惯性行者',
icon: 'assets/icons/habitual.png',
desc: '在惯性中运转,内在驱动力和自我觉察都较弱',
quote: '「慢慢来比较快」',
analysis: '你可能感觉自己像一台被程序化运转的机器——每天做着差不多的事,想着差不多的念头,每一念都来自过去。这不是你的问题,而是每个人都可能陷入的状态,课程中称之为"空心人"或"NPC"。但旧地图无法发现新大陆,看见即觉察,觉察即涨心。当你开始看见自己的惯性,改变就已经发生了。',
recommend: '看见即觉察,觉察即涨心。课程将帮你走出惯性,找回内在力量,从"NPC"变回自己。'
}
};
// ========================================
// 状态管理
// ========================================
let currentQuestion = 0; // 当前题目索引0-17
let answers = []; // 答题记录长度18每项1-5
// localStorage 键名
const STORAGE_KEY = 'heritage_gene_test';
// ========================================
// DOM 元素引用
// ========================================
const $home = document.getElementById('home');
const $quiz = document.getElementById('quiz');
const $loading = document.getElementById('loading');
const $result = document.getElementById('result');
const $poster = document.getElementById('poster');
const $progressFill = document.getElementById('progress-fill');
const $progressText = document.getElementById('progress-text');
const $quizQuestion = document.getElementById('quiz-question');
const $questionNumber = document.getElementById('question-number');
const $questionInner = document.getElementById('question-wrapper').querySelector('.question-inner');
const $scaleButtons = document.getElementById('scale-buttons');
const $btnPrev = document.getElementById('btn-prev');
const $resultIcon = document.getElementById('result-icon');
const $resultName = document.getElementById('result-name');
const $resultDesc = document.getElementById('result-desc');
const $resultAnalysis = document.getElementById('result-analysis');
const $resultRecommend = document.getElementById('result-recommend');
const $radarCanvas = document.getElementById('radar-canvas');
const $posterIcon = document.getElementById('poster-icon');
const $posterName = document.getElementById('poster-name');
const $posterDesc = document.getElementById('poster-desc');
const $posterQuote = document.getElementById('poster-quote');
const $posterRadarCanvas = document.getElementById('poster-radar-canvas');
const $posterContent = document.getElementById('poster-content');
const $posterPreview = document.getElementById('poster-preview');
const $posterImage = document.getElementById('poster-image');
const $privacyModal = document.getElementById('privacy-modal');
const $courseModal = document.getElementById('course-modal');
const $courseModalImg = document.getElementById('course-modal-img');
const $courseCardImg = document.getElementById('course-card-img');
// ========================================
// 初始化
// ========================================
function init() {
// 尝试从 localStorage 恢复进度
loadProgress();
// 绑定事件
document.getElementById('btn-start').addEventListener('click', startQuiz);
document.getElementById('btn-prev').addEventListener('click', prevQuestion);
document.getElementById('btn-share').addEventListener('click', generatePoster);
document.getElementById('btn-retry').addEventListener('click', retryQuiz);
document.getElementById('btn-privacy').addEventListener('click', function () {
$privacyModal.style.display = 'flex';
});
document.getElementById('btn-close-privacy').addEventListener('click', function () {
$privacyModal.style.display = 'none';
});
document.getElementById('btn-poster-close').addEventListener('click', closePoster);
// 点击遮罩关闭弹窗
$privacyModal.addEventListener('click', function (e) {
if (e.target === $privacyModal) {
$privacyModal.style.display = 'none';
}
});
// 课程卡片:点击查看大图
$courseCardImg.addEventListener('click', function () {
$courseModalImg.src = $courseCardImg.src;
$courseModal.style.display = 'flex';
});
// 课程大图:点击遮罩关闭
$courseModal.addEventListener('click', function (e) {
if (e.target === $courseModal) {
$courseModal.style.display = 'none';
}
});
// 量表按钮事件委托
$scaleButtons.addEventListener('click', function (e) {
var btn = e.target.closest('.btn-scale');
if (!btn) return;
var value = parseInt(btn.getAttribute('data-value'));
selectAnswer(value);
});
}
// ========================================
// 页面切换
// ========================================
function showPage(pageId) {
var pages = document.querySelectorAll('.page');
pages.forEach(function (p) {
p.classList.remove('active');
});
document.getElementById(pageId).classList.add('active');
// 滚动到顶部
window.scrollTo(0, 0);
}
// ========================================
// 答题流程
// ========================================
/** 开始测试 */
function startQuiz() {
currentQuestion = 0;
answers = new Array(questions.length).fill(0);
saveProgress();
showPage('quiz');
renderQuestion();
}
/** 渲染当前题目(带滑动动画) */
function renderQuestion(direction) {
// direction: 'next' | 'prev' | undefined首次
var doRender = function() {
// 更新进度
var progress = ((currentQuestion) / questions.length) * 100;
$progressFill.style.width = progress + '%';
$progressText.textContent = (currentQuestion + 1) + ' / ' + questions.length;
// 更新题目
$questionNumber.textContent = 'Q' + (currentQuestion + 1);
$quizQuestion.textContent = questions[currentQuestion];
// 更新量表按钮状态
var btns = $scaleButtons.querySelectorAll('.btn-scale');
var currentAnswer = answers[currentQuestion];
btns.forEach(function (btn) {
var val = parseInt(btn.getAttribute('data-value'));
if (val === currentAnswer) {
btn.classList.add('selected');
} else {
btn.classList.remove('selected');
}
});
// 重置选项弹入动画
resetScaleAnimation();
// 上一题按钮
$btnPrev.style.visibility = currentQuestion > 0 ? 'visible' : 'hidden';
// 滑入
if (direction === 'next') {
$questionInner.classList.remove('slide-in-right', 'slide-out-left', 'slide-in-left', 'slide-out-right');
$questionInner.classList.add('slide-in-right');
requestAnimationFrame(function() {
requestAnimationFrame(function() {
$questionInner.classList.remove('slide-in-right');
});
});
} else if (direction === 'prev') {
$questionInner.classList.remove('slide-in-right', 'slide-out-left', 'slide-in-left', 'slide-out-right');
$questionInner.classList.add('slide-in-left');
requestAnimationFrame(function() {
requestAnimationFrame(function() {
$questionInner.classList.remove('slide-in-left');
});
});
}
};
if (!direction) {
doRender();
return;
}
// 先滑出当前题目
var outClass = direction === 'next' ? 'slide-out-left' : 'slide-out-right';
$questionInner.classList.remove('slide-in-right', 'slide-out-left', 'slide-in-left', 'slide-out-right');
$questionInner.classList.add(outClass);
setTimeout(function() {
doRender();
}, 250);
}
/** 重置量表按钮的弹入动画 */
function resetScaleAnimation() {
var btns = $scaleButtons.querySelectorAll('.btn-scale');
btns.forEach(function(btn) {
btn.style.animation = 'none';
btn.offsetHeight; // 触发重排
btn.style.animation = '';
});
}
/** 选择答案 */
function selectAnswer(value) {
answers[currentQuestion] = value;
saveProgress();
// 高亮选中按钮
var btns = $scaleButtons.querySelectorAll('.btn-scale');
btns.forEach(function (btn) {
var val = parseInt(btn.getAttribute('data-value'));
if (val === value) {
btn.classList.add('selected');
} else {
btn.classList.remove('selected');
}
});
// 更新进度条到当前题的完成状态
var progress = ((currentQuestion + 1) / questions.length) * 100;
$progressFill.style.width = progress + '%';
// 0.3秒后自动跳下一题
setTimeout(function () {
if (currentQuestion < questions.length - 1) {
currentQuestion++;
renderQuestion('next');
} else {
// 最后一题,进入计算页
showLoading();
}
}, 300);
}
/** 上一题 */
function prevQuestion() {
if (currentQuestion > 0) {
currentQuestion--;
renderQuestion('prev');
}
}
// ========================================
// 计算页
// ========================================
function showLoading() {
showPage('loading');
// 2.5秒后跳转结果页
setTimeout(function () {
calculateAndShowResult();
}, 2500);
}
// ========================================
// 计分与类型匹配
// ========================================
function calculateAndShowResult() {
// 计算各维度分数每维度3题分值3-15
var dimensions = [];
for (var d = 0; d < 6; d++) {
var sum = 0;
for (var q = 0; q < 3; q++) {
sum += answers[d * 3 + q];
}
dimensions.push(sum);
}
// 维度:觉察、情绪、关系、传承、驱动、成长
var 觉察 = dimensions[0];
var 情绪 = dimensions[1];
var 关系 = dimensions[2];
var 传承 = dimensions[3];
var 驱动 = dimensions[4];
var 成长 = dimensions[5];
// 类型匹配(按优先级)
var typeKey;
if (觉察 >= 10 && 情绪 >= 10) {
typeKey = 'awakener';
} else if (传承 >= 10 && 关系 >= 10) {
typeKey = 'icebreaker';
} else if (情绪 >= 10 && 觉察 <= 9) {
typeKey = 'pauser';
} else if (传承 >= 10 && 成长 >= 10 && 驱动 <= 9) {
typeKey = 'guardian';
} else if (成长 >= 10 && 驱动 >= 10 && 觉察 <= 9) {
typeKey = 'seeker';
} else if (关系 <= 9 && 情绪 <= 9) {
typeKey = 'relational';
} else if (驱动 <= 9 && 觉察 <= 9) {
typeKey = 'habitual';
} else {
typeKey = 'sleeper';
}
var typeData = types[typeKey];
// 渲染结果页
$resultIcon.src = typeData.icon;
$resultIcon.alt = typeData.name;
$resultName.textContent = typeData.name;
$resultDesc.textContent = typeData.desc;
$resultAnalysis.textContent = typeData.analysis;
$resultRecommend.textContent = typeData.recommend;
// 绘制雷达图
drawRadarChart($radarCanvas, dimensions, 300);
// 保存结果到 localStorage
localStorage.setItem(STORAGE_KEY + '_result', JSON.stringify({
typeKey: typeKey,
dimensions: dimensions
}));
// 清除答题进度
localStorage.removeItem(STORAGE_KEY);
showPage('result');
}
// ========================================
// 雷达图绘制Canvas API
// ========================================
function drawRadarChart(canvas, dimensions, size) {
var ctx = canvas.getContext('2d');
var dpr = window.devicePixelRatio || 1;
// 高清屏适配
canvas.width = size * dpr;
canvas.height = size * dpr;
canvas.style.width = size + 'px';
canvas.style.height = size + 'px';
ctx.scale(dpr, dpr);
var centerX = size / 2;
var centerY = size / 2;
var maxRadius = size * 0.3;
var levels = 5; // 5层网格
var sides = 6; // 6个维度
var angleStep = (Math.PI * 2) / sides;
var startAngle = -Math.PI / 2; // 从顶部开始
ctx.clearRect(0, 0, size, size);
// 绘制背景网格5层
for (var level = 1; level <= levels; level++) {
var r = (maxRadius / levels) * level;
ctx.beginPath();
for (var i = 0; i <= sides; i++) {
var angle = startAngle + angleStep * i;
var x = centerX + r * Math.cos(angle);
var y = centerY + r * Math.sin(angle);
if (i === 0) {
ctx.moveTo(x, y);
} else {
ctx.lineTo(x, y);
}
}
ctx.closePath();
ctx.strokeStyle = 'rgba(255, 255, 255, 0.08)';
ctx.lineWidth = 1;
ctx.stroke();
}
// 绘制轴线
for (var i = 0; i < sides; i++) {
var angle = startAngle + angleStep * i;
ctx.beginPath();
ctx.moveTo(centerX, centerY);
ctx.lineTo(
centerX + maxRadius * Math.cos(angle),
centerY + maxRadius * Math.sin(angle)
);
ctx.strokeStyle = 'rgba(255, 255, 255, 0.1)';
ctx.lineWidth = 1;
ctx.stroke();
}
// 绘制数据区域
ctx.beginPath();
for (var i = 0; i < sides; i++) {
// 维度分值范围3-15映射到0-1
var value = (dimensions[i] - 3) / 12;
value = Math.max(0, Math.min(1, value));
var r = maxRadius * value;
var angle = startAngle + angleStep * i;
var x = centerX + r * Math.cos(angle);
var y = centerY + r * Math.sin(angle);
if (i === 0) {
ctx.moveTo(x, y);
} else {
ctx.lineTo(x, y);
}
}
ctx.closePath();
// 渐变填充
var gradient = ctx.createRadialGradient(centerX, centerY, 0, centerX, centerY, maxRadius);
gradient.addColorStop(0, 'rgba(124, 92, 191, 0.45)');
gradient.addColorStop(1, 'rgba(240, 194, 122, 0.2)');
ctx.fillStyle = gradient;
ctx.fill();
ctx.strokeStyle = 'rgba(196, 181, 253, 0.6)';
ctx.lineWidth = 2;
ctx.stroke();
// 绘制数据点
for (var i = 0; i < sides; i++) {
var value = (dimensions[i] - 3) / 12;
value = Math.max(0, Math.min(1, value));
var r = maxRadius * value;
var angle = startAngle + angleStep * i;
var x = centerX + r * Math.cos(angle);
var y = centerY + r * Math.sin(angle);
ctx.beginPath();
ctx.arc(x, y, 4, 0, Math.PI * 2);
ctx.fillStyle = '#f0c27a';
ctx.fill();
ctx.strokeStyle = 'rgba(240, 194, 122, 0.4)';
ctx.lineWidth = 6;
ctx.stroke();
}
// 绘制维度标签
ctx.fillStyle = 'rgba(255, 255, 255, 0.7)';
ctx.font = '11px "Noto Sans SC", -apple-system, BlinkMacSystemFont, "PingFang SC", "Microsoft YaHei", sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
var labelRadius = maxRadius + 24;
for (var i = 0; i < sides; i++) {
var angle = startAngle + angleStep * i;
var x = centerX + labelRadius * Math.cos(angle);
var y = centerY + labelRadius * Math.sin(angle);
ctx.fillText(dimensionNames[i], x, y);
}
}
// ========================================
// 海报生成
// ========================================
function generatePoster() {
// 获取结果数据
var resultData = JSON.parse(localStorage.getItem(STORAGE_KEY + '_result') || '{}');
if (!resultData.typeKey) return;
var typeData = types[resultData.typeKey];
var dimensions = resultData.dimensions;
// 填充海报内容
$posterIcon.src = typeData.icon;
$posterName.textContent = typeData.name;
$posterDesc.textContent = typeData.desc;
$posterQuote.textContent = typeData.quote;
// 在海报canvas上重新绘制雷达图
drawRadarChart($posterRadarCanvas, dimensions, 400);
// 显示海报容器
$poster.classList.add('active');
$posterContent.style.display = 'block';
$posterPreview.style.display = 'none';
// 等待图片和canvas渲染完成后截图
setTimeout(function () {
capturePoster();
}, 500);
}
/** 使用 html2canvas 截图 */
function capturePoster() {
if (typeof html2canvas === 'undefined') {
alert('海报生成组件加载失败,请刷新页面重试');
return;
}
// 先显示加载提示
$posterPreview.style.display = 'block';
$posterImage.style.display = 'none';
var previewTitle = document.querySelector('.poster-preview-title');
if (previewTitle) previewTitle.textContent = '正在生成海报...';
html2canvas($posterContent, {
scale: 2,
useCORS: true,
allowTaint: true,
backgroundColor: '#0f0a1e',
width: 750,
height: 1334,
logging: false
}).then(function (canvas) {
$posterContent.style.display = 'none';
var dataUrl = canvas.toDataURL('image/png');
$posterImage.src = dataUrl;
$posterImage.style.display = 'block';
if (previewTitle) previewTitle.textContent = '长按图片保存分享';
}).catch(function (err) {
console.error('海报生成失败:', err);
$posterContent.style.display = 'none';
$posterPreview.style.display = 'block';
$posterImage.style.display = 'none';
if (previewTitle) previewTitle.textContent = '海报生成失败,请重试';
});
}
/** 关闭海报 */
function closePoster() {
$poster.classList.remove('active');
$posterContent.style.display = 'block';
$posterPreview.style.display = 'none';
}
// ========================================
// 重新测试
// ========================================
function retryQuiz() {
localStorage.removeItem(STORAGE_KEY);
localStorage.removeItem(STORAGE_KEY + '_result');
answers = new Array(questions.length).fill(0);
currentQuestion = 0;
showPage('home');
}
// ========================================
// localStorage 进度保存/恢复
// ========================================
function saveProgress() {
var data = {
currentQuestion: currentQuestion,
answers: answers
};
try {
localStorage.setItem(STORAGE_KEY, JSON.stringify(data));
} catch (e) {
// localStorage 不可用时静默失败
}
}
function loadProgress() {
try {
var raw = localStorage.getItem(STORAGE_KEY);
if (raw) {
var data = JSON.parse(raw);
if (data.answers && data.answers.length === questions.length) {
answers = data.answers;
currentQuestion = data.currentQuestion || 0;
// 如果有未完成的测试,恢复到答题页
var allAnswered = answers.every(function (a) { return a > 0; });
if (allAnswered) {
// 所有题都答完了,直接计算结果
calculateAndShowResult();
} else if (currentQuestion > 0) {
// 有部分进度,恢复答题
showPage('quiz');
renderQuestion();
}
}
}
} catch (e) {
// 解析失败时忽略
}
}
// ========================================
// 启动
// ========================================
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();