redesign homepage: 测测-style layout, icon grid for personality/zodiac tests, remove clinical terminology from headers

This commit is contained in:
jackyu66git
2026-08-02 14:25:37 +08:00
parent e6ad46fe87
commit 2686866376
14 changed files with 25369 additions and 168 deletions
+316
View File
@@ -0,0 +1,316 @@
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width,initial-scale=1.0,user-scalable=no">
<title>量表测试 — 愈心谷</title>
<style>
:root{--pri:#E54D42;--bg-start:#FFD1C7;--bg-end:#FFC8B5}
*{box-sizing:border-box;margin:0;padding:0}
body{
font-family:-apple-system,BlinkMacSystemFont,"PingFang SC","Microsoft YaHei",sans-serif;
background:linear-gradient(180deg,var(--bg-start) 0%,var(--bg-end) 50%,#fff5f0 100%);
min-height:100vh;-webkit-font-smoothing:antialiased;
display:flex;flex-direction:column;
}
.top-bar{display:flex;align-items:center;padding:14px 16px;gap:12px;max-width:500px;margin:0 auto}
.top-bar .back{font-size:22px;text-decoration:none;color:#666;line-height:1}
.top-bar .title{flex:1;font-size:16px;font-weight:600;color:#333;white-space:nowrap;overflow:hidden;text-overflow:ellipsis;text-align:center}
.top-bar .qnum{font-size:13px;color:#999}
.progress{height:3px;background:rgba(0,0,0,0.08);max-width:500px;margin:0 auto}
.progress .bar{height:100%;background:var(--pri);transition:width 0.3s}
.main{flex:1;display:flex;flex-direction:column;padding:20px;max-width:500px;width:100%;margin:0 auto}
.qcard{background:#fff;border-radius:14px;padding:28px 22px;box-shadow:0 2px 10px rgba(0,0,0,0.04);flex:1}
.qcard .qtext{font-size:17px;color:#333;line-height:1.7;margin-bottom:24px}
.qcard .qtext .qidx{color:var(--pri);font-weight:600;margin-right:6px}
.options{display:flex;flex-direction:column;gap:10px}
.option{
padding:14px 18px;border-radius:10px;border:1.5px solid #eee;
font-size:15px;color:#444;cursor:pointer;transition:all 0.15s;text-align:left;background:#fafafa;
}
.option:hover{border-color:var(--pri);background:#fff5f3}
.option.selected{border-color:var(--pri);background:#fff0ed;color:var(--pri);font-weight:500}
.btns{display:flex;gap:12px;padding:16px 0 30px;max-width:500px;width:100%;margin:0 auto}
.btns button{
flex:1;padding:14px;border-radius:10px;border:none;font-size:15px;cursor:pointer;
font-weight:500;transition:all 0.15s;
}
.btn-prev{background:#f0f0f0;color:#666}
.btn-next{background:var(--pri);color:#fff}
.btn-submit{background:#4CAF50;color:#fff}
button:disabled{opacity:0.4;cursor:not-allowed}
.submit-area{text-align:center;padding:40px 20px}
.submit-area p{font-size:14px;color:#888;margin-bottom:20px}
.submit-area button{
padding:14px 40px;border-radius:10px;background:var(--pri);color:#fff;
border:none;font-size:16px;cursor:pointer;font-weight:500;
}
.result-card{background:#fff;border-radius:14px;padding:32px 22px;box-shadow:0 2px 10px rgba(0,0,0,0.04);text-align:center;max-width:500px;width:100%;margin:0 auto}
.result-card .score{font-size:48px;font-weight:700;color:var(--pri);margin:12px 0}
.result-card .level{font-size:18px;color:#666;margin-bottom:20px}
.result-card .detail{font-size:14px;color:#999;line-height:1.6;text-align:left}
.result-card .back-btn{
display:inline-block;margin-top:20px;padding:12px 30px;background:var(--pri);color:#fff;
border-radius:10px;text-decoration:none;font-size:15px;
}
.loading{text-align:center;padding:80px 20px;color:#999}
</style>
</head>
<body>
<div id="app"><div class="loading">加载中…</div></div>
<script>
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();
</script>
</body>
</html>