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>
243 lines
10 KiB
JavaScript
243 lines
10 KiB
JavaScript
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();
|