This commit is contained in:
2026-04-26 13:47:21 +08:00
commit 4983f34202
24 changed files with 2559 additions and 0 deletions

684
js/app.js Normal file
View File

@@ -0,0 +1,684 @@
/**
* 传承基因测试 - 主逻辑
* 包含:题目数据、类型数据、答题流程、计分逻辑、雷达图绘制、海报生成
*/
(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();
}
})();