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