Files
digital-psychology/js/scale-test.js
T
jackyu66gitandCursor 2fb1dfee14 chore: seal Design Vision v1 and monorepo scaffold
Archive the differentiated YuXinGu product docs, AI engineering system,
design contract, and Go/Vue scaffold. Next execution prioritizes Cece-parity
over early innovation (see .ai/product/STRATEGY.md).

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-02 16:00:44 +08:00

243 lines
10 KiB
JavaScript
Raw 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.
const params = new URLSearchParams(location.search);
const slug = params.get('slug');
let scale = null, answers = [], current = 0, submitted = false, started = false;
async function init() {
if (!slug) { document.getElementById('app').innerHTML = '<div class="loading">缺少量表参数</div>'; return; }
try {
const r = await fetch(`../api/scales/${slug}`);
scale = await r.json();
if (scale.error) throw new Error(scale.error);
answers = new Array(scale.question_count).fill(null);
// Restore from localStorage
const cached = localStorage.getItem(`scale_${slug}`);
if (cached) answers = JSON.parse(cached);
renderStart();
} catch(e) {
document.getElementById('app').innerHTML = `<div class="loading">加载失败: ${e.message}</div>`;
}
}
function renderStart() {
const hasCache = answers.some(a => a !== null);
const answered = answers.filter(a => a !== null).length;
const rawDesc = scale.description || '';
const parts = rawDesc.split('\n\n');
const mainDesc = parts[0] || '';
const metaLine = parts.slice(1).join(' · ') || '';
const timeMin = Math.ceil(scale.question_count / 6);
const timeMax = Math.ceil(scale.question_count / 4);
document.getElementById('app').innerHTML = `
<div style="max-width:500px;margin:0 auto;position:relative;text-align:center;padding:32px 20px 8px">
<a href="scales.html" style="position:absolute;left:20px;top:34px;color:#999;font-size:20px;text-decoration:none;line-height:1">←</a>
<div style="font-size:20px;font-weight:700;color:#333">${scale.name}</div>
</div>
<div class="main" style="justify-content:center;padding:8px 20px 16px">
<div class="qcard" style="text-align:left;flex:none;padding:24px 20px">
${mainDesc ? `<div style="font-size:15px;color:#444;line-height:1.85;margin-bottom:16px">${mainDesc}</div>` : ''}
${metaLine ? `<div style="display:flex;flex-wrap:wrap;gap:6px;margin-bottom:16px">
${metaLine.split('·').filter(m=>m.trim()).map(m =>
`<span style="display:inline-block;padding:4px 10px;background:#f5f5f5;border-radius:6px;font-size:12px;color:#888">${m.trim()}</span>`
).join('')}
</div>` : ''}
<div style="display:flex;align-items:center;gap:12px;padding:12px 14px;background:#f9f9f9;border-radius:8px;margin-bottom:16px;font-size:13px;color:#888">
<span>📝 <b style="color:#555">${scale.question_count}</b> 题</span>
<span>⏱ ${timeMin}-${timeMax} 分钟</span>
<span>🔒 自动保存</span>
</div>
${hasCache
? `<div style="background:#fff5f3;padding:12px;border-radius:8px;margin-bottom:14px;font-size:13px;color:var(--pri);text-align:center">
📌 已有答题进度:<b>${answered}/${scale.question_count}</b> 题
</div>`
: ''}
<button onclick="startTest()" style="display:block;width:100%;padding:15px;border:none;border-radius:10px;background:var(--pri);color:#fff;font-size:17px;font-weight:600;cursor:pointer">
${hasCache ? `继续测试 (${answered}/${scale.question_count})` : '开始测试'}
</button>
${hasCache
? `<button onclick="resetAndStart()" style="display:block;width:100%;padding:12px;border:1px solid #ddd;border-radius:10px;background:#fff;color:#999;font-size:14px;cursor:pointer;margin-top:8px">重新开始</button>`
: ''}
</div>
</div>
`;
}
function startTest() {
started = true;
// Resume from first unanswered question
const firstUnanswered = answers.findIndex(a => a === null);
current = firstUnanswered >= 0 ? firstUnanswered : 0;
render();
}
function resetAndStart() {
answers = new Array(scale.question_count).fill(null);
localStorage.removeItem(`scale_${slug}`);
started = true;
current = 0;
render();
}
function saveCache() {
localStorage.setItem(`scale_${slug}`, JSON.stringify(answers));
}
function selectOption(idx) {
if (submitted) return;
answers[current] = idx;
saveCache();
render();
// Auto advance after short delay
if (current < scale.question_count - 1) {
setTimeout(() => { current++; render(); }, 300);
}
}
function goPrev() { if (current > 0) { current--; render(); } }
function goNext() { if (current < scale.question_count - 1) { current++; render(); } }
async function submit() {
if (submitted) return;
submitted = true;
saveCache();
try {
const r = await fetch('../api/scales/result', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
scale_slug: scale.slug,
scale_name: scale.name,
answers: answers,
})
});
const data = await r.json();
// Clear cache
localStorage.removeItem(`scale_${slug}`);
renderResult(data);
} catch(e) {
submitted = false;
alert('提交失败,请重试');
render();
}
}
function renderResult(data) {
const s = data.scores;
const allAnswered = s.answer_count;
const total = scale.question_count;
const scoreDisplay = s.std_score !== null ? s.std_score : s.raw_score;
const isStd = s.std_score !== null;
const isFactorScale = s.factors && Object.keys(s.factors).length >= 6;
const hasFactors = s.factors && Object.keys(s.factors).length > 0;
// Factor bars (for any factor count, not just 8+)
let factorsHtml = '';
if (hasFactors) {
const factorMax = s.factor_max || {};
const bars = Object.entries(s.factors).map(([k,v]) => {
const max = factorMax[k] || 50;
const pct = Math.min(Math.round(v / max * 100), 100);
return `<div style="margin:6px 0">
<div style="display:flex;justify-content:space-between;font-size:12px;margin-bottom:2px">
<span style="color:#666">${k}</span><span style="color:var(--pri);font-weight:600">${v}<span style="color:#bbb;font-weight:400">/${max}</span></span>
</div>
<div style="height:6px;background:#f0f0f0;border-radius:3px;overflow:hidden">
<div style="height:100%;width:${pct}%;background:linear-gradient(90deg,var(--pri),#F0987A);border-radius:3px;transition:width 0.5s"></div>
</div>
</div>`;
}).join('');
let insightHtml = '';
if (isFactorScale) {
const sorted = Object.entries(s.factors).sort((a,b) => b[1]-a[1]);
const top3 = sorted.slice(0,3).map(([k,v]) => `<b>${k}</b>`).join('、');
insightHtml = `<div style="margin-top:14px;padding:12px;background:#fff5f3;border-radius:8px;font-size:13px;line-height:1.7">
🌟 <b>优势方向:</b>${top3}<br>
<span style="color:#999">得分最高的领域代表天赋倾向,建议重点关注和培养。</span>
</div>`;
}
factorsHtml = `<div style="margin:20px 0 0;text-align:left">
<div style="font-size:14px;font-weight:600;color:#555;margin-bottom:8px">${isFactorScale ? '各维度得分' : '因子得分'}</div>
${bars}
${insightHtml}
</div>`;
}
// Rating badge
let ratingHtml = '';
if (s.rating) {
ratingHtml = `<div style="display:inline-block;margin-top:8px;padding:6px 18px;background:${isFactorScale?'#fff5f3':'#fef0ed'};border-radius:20px;font-size:15px;font-weight:600;color:var(--pri)">${s.rating}</div>`;
}
document.getElementById('app').innerHTML = `
<div class="top-bar">
<a href="scales.html" class="back">←</a>
<div class="title">${scale.name}</div>
</div>
<div class="result-card" style="text-align:center">
<div style="font-size:13px;color:#bbb;margin-bottom:4px">测评完成 · ${allAnswered}/${total} 题</div>
${isFactorScale
? `<div style="font-size:22px;font-weight:700;color:#333;margin:8px 0 4px">多元智能测评结果</div>`
: `<div style="margin:8px 0">
<span style="font-size:56px;font-weight:700;color:var(--pri);line-height:1">${scoreDisplay}</span>
${isStd ? `<span style="font-size:16px;color:#999;margin-left:4px">分</span>` : ''}
</div>
${isStd ? `<div style="font-size:12px;color:#bbb;margin-top:-4px">原始分 ${s.raw_score} × 1.25 = 标准分 ${scoreDisplay}</div>` : ''}
${!isStd && !isFactorScale ? `<div style="font-size:13px;color:#999">满分 ${s.max_score} · 占比 ${(s.raw_score/s.max_score*100).toFixed(0)}%</div>` : ''}`}
${ratingHtml}
${factorsHtml}
<div style="margin-top:24px;display:flex;gap:10px;justify-content:center">
<a href="scales.html" style="flex:1;padding:12px;background:var(--pri);color:#fff;border-radius:10px;text-decoration:none;font-size:14px;font-weight:500">返回量表库</a>
<a href="scale-test.html?slug=${slug}" style="flex:1;padding:12px;background:#f5f5f5;color:#666;border-radius:10px;text-decoration:none;font-size:14px">重新测试</a>
</div>
<p style="margin-top:16px;color:#ccc;font-size:11px">* 结果仅供参考,不能替代专业诊断</p>
</div>
`;
}
function render() {
if (!scale) return;
const total = scale.question_count;
const pct = ((current + 1) / total * 100).toFixed(0);
const isLast = current === total - 1;
const selected = answers[current];
const q = scale.questions[current];
const opts = scale.options[current];
const optHtml = opts.map((o, i) => `
<button class="option${selected === i ? ' selected' : ''}" onclick="selectOption(${i})">
${String.fromCharCode(65+i)}. ${o}
</button>
`).join('');
const allAnswered = answers.every(a => a !== null);
document.getElementById('app').innerHTML = `
<div class="top-bar">
<a href="scales.html" class="back">←</a>
<div class="title">${scale.name}</div>
<div class="qnum">${current+1}/${total}</div>
</div>
<div class="progress"><div class="bar" style="width:${pct}%"></div></div>
<div class="main">
<div class="qcard">
<div class="qtext"><span class="qidx">${current+1}.</span>${q}</div>
<div class="options">${optHtml}</div>
</div>
</div>
<div class="btns">
<button class="btn-prev" onclick="goPrev()" ${current===0?'disabled':''}>上一题</button>
${isLast
? `<button class="btn-submit" onclick="submit()" ${allAnswered?'':'disabled'}>✓ 提交答卷</button>`
: `<button class="btn-next" onclick="goNext()" ${selected===null?'disabled':''}>下一题 →</button>`
}
</div>
${isLast ? `<div style="text-align:center;padding:0 20px 20px;font-size:12px;color:#999">${allAnswered?'全部答完,可以提交了':'还有题目未作答'}</div>` : ''}
`;
}
init();