b6df028839
变更摘要: - [fix] 清理根目录 index.html 遗留代码(questionnaire-error + retryLoad 移除) - [fix] 副标题同步更新为自估591分 + 候选志愿546条数据校准 - [fix] 页脚注入版本号 v1.0.0 - [fix] 部署版 index.html: 取消家长标签,统一 filler-tianheng - [fix] Dashboard 文案修正(4份→2份线上问卷) - [fix] 清理冗余部署脚本(v1-v4归档至 archived_scripts/) - [fix] 等效位次工具纳入 deploy 目录 - [chore] .gitignore 初始化 - [init] Git 仓库初始化
681 lines
33 KiB
HTML
681 lines
33 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>霍兰德职业兴趣测试 · 自评版</title>
|
|
<style>
|
|
:root {
|
|
--primary: #1a3a5c;
|
|
--primary-light: #2c5282;
|
|
--gold: #c9a84c;
|
|
--gold-light: #f0e6c8;
|
|
--bg: #f5f0e8;
|
|
--card: #ffffff;
|
|
--text: #2d3748;
|
|
--text-light: #718096;
|
|
--border: #e2e8f0;
|
|
--success: #38a169;
|
|
--accent-r: #e53e3e;
|
|
--accent-i: #3182ce;
|
|
--accent-a: #805ad5;
|
|
--accent-s: #38a169;
|
|
--accent-e: #dd6b20;
|
|
--accent-c: #718096;
|
|
}
|
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
body { font-family: "PingFang SC","Microsoft YaHei","Noto Sans SC",sans-serif; background: var(--bg); color: var(--text); line-height: 1.8; padding-bottom: 80px; }
|
|
|
|
/* Header */
|
|
.header { background: linear-gradient(135deg, var(--primary) 0%, var(--primary-light) 100%); color: #fff; padding: 40px 20px; text-align: center; position: relative; overflow: hidden; }
|
|
.header::after { content:""; position: absolute; top: -50%; right: -20%; width: 300px; height: 300px; background: rgba(201,168,76,0.1); border-radius: 50%; }
|
|
.header h1 { font-size: 28px; font-weight: 700; margin-bottom: 8px; position: relative; z-index: 1; }
|
|
.header .subtitle { font-size: 15px; opacity: 0.85; position: relative; z-index: 1; }
|
|
.header .badge { display: inline-block; background: var(--gold); color: var(--primary); padding: 4px 16px; border-radius: 20px; font-size: 13px; font-weight: 600; margin-top: 12px; position: relative; z-index: 1; }
|
|
|
|
/* Container */
|
|
.container { max-width: 720px; margin: 0 auto; padding: 20px; }
|
|
|
|
/* Intro Card */
|
|
.intro-card { background: var(--card); border-radius: 12px; padding: 28px; margin: 24px 0; box-shadow: 0 1px 3px rgba(0,0,0,0.08); border-left: 4px solid var(--primary); }
|
|
.intro-card h3 { font-size: 17px; color: var(--primary); margin-bottom: 8px; }
|
|
.intro-card p { font-size: 14px; color: var(--text-light); margin-bottom: 6px; }
|
|
|
|
/* Dimension Nav */
|
|
.dim-nav { display: flex; flex-wrap: wrap; gap: 8px; margin: 20px 0; position: sticky; top: 10px; z-index: 10; background: var(--bg); padding: 12px 0; }
|
|
.dim-nav button { padding: 8px 16px; border: 2px solid var(--border); border-radius: 20px; background: var(--card); cursor: pointer; font-size: 14px; font-weight: 600; transition: all 0.2s; color: var(--text); }
|
|
.dim-nav button:hover { border-color: var(--primary-light); }
|
|
.dim-nav button.active { background: var(--primary); color: #fff; border-color: var(--primary); }
|
|
.dim-nav button.done { border-color: var(--success); color: var(--success); }
|
|
.dim-nav button.done::after { content: " ✓"; }
|
|
|
|
/* Section */
|
|
.section { background: var(--card); border-radius: 12px; padding: 32px 28px; margin: 20px 0; box-shadow: 0 1px 3px rgba(0,0,0,0.08); }
|
|
.section h2 { font-size: 22px; color: var(--primary); margin-bottom: 4px; display: flex; align-items: center; gap: 10px; }
|
|
.section .dim-code { display: inline-flex; align-items: center; justify-content: center; width: 36px; height: 36px; border-radius: 50%; font-weight: 700; font-size: 18px; color: #fff; flex-shrink: 0; }
|
|
.section .dim-desc { font-size: 14px; color: var(--text-light); margin-bottom: 20px; }
|
|
|
|
/* Question */
|
|
.question { padding: 14px 0; border-bottom: 1px solid var(--border); display: flex; align-items: center; justify-content: space-between; gap: 16px; }
|
|
.question:last-child { border-bottom: none; }
|
|
.question .q-text { flex: 1; font-size: 15px; }
|
|
.question .q-num { color: var(--text-light); font-size: 13px; margin-right: 6px; }
|
|
|
|
/* Rating */
|
|
.rating { display: flex; gap: 4px; flex-shrink: 0; }
|
|
.rating button { width: 44px; height: 36px; border: 2px solid var(--border); border-radius: 6px; background: var(--card); cursor: pointer; font-size: 13px; font-weight: 600; transition: all 0.15s; color: var(--text-light); }
|
|
.rating button:hover { border-color: var(--primary-light); background: #ebf4ff; }
|
|
.rating button.sel-zero { background: #fed7d7; border-color: var(--accent-r); color: var(--accent-r); }
|
|
.rating button.sel-one { background: #fefcbf; border-color: var(--gold); color: #b7791f; }
|
|
.rating button.sel-two { background: #c6f6d5; border-color: var(--success); color: #276749; }
|
|
.rating .r-label { font-size: 10px; color: var(--text-light); text-align: center; line-height: 1.2; }
|
|
|
|
/* Progress */
|
|
.progress-wrap { background: var(--card); border-radius: 12px; padding: 20px 28px; margin: 20px 0; box-shadow: 0 1px 3px rgba(0,0,0,0.08); }
|
|
.progress-bar { height: 8px; background: var(--border); border-radius: 4px; overflow: hidden; margin-bottom: 8px; }
|
|
.progress-fill { height: 100%; background: linear-gradient(90deg, var(--primary-light), var(--gold)); border-radius: 4px; transition: width 0.3s; }
|
|
.progress-text { font-size: 14px; color: var(--text-light); text-align: center; }
|
|
|
|
/* Results */
|
|
.results { background: var(--card); border-radius: 12px; padding: 32px 28px; margin: 20px 0; box-shadow: 0 1px 3px rgba(0,0,0,0.08); display: none; overflow: hidden; }
|
|
.results.show { display: block; overflow: visible; }
|
|
.results h2 { font-size: 24px; color: var(--primary); text-align: center; margin-bottom: 24px; }
|
|
.result-code { text-align: center; margin: 20px 0; }
|
|
.result-code .code-letters { font-size: 64px; font-weight: 900; letter-spacing: 8px; background: linear-gradient(135deg, var(--primary), var(--gold)); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; }
|
|
.result-code .code-label { font-size: 15px; color: var(--text-light); margin-top: 4px; }
|
|
|
|
.bar-chart { margin: 24px 0; }
|
|
.bar-row { display: flex; align-items: center; margin: 10px 0; gap: 12px; }
|
|
.bar-label { width: 80px; text-align: right; font-size: 14px; font-weight: 600; }
|
|
.bar-track { flex: 1; height: 28px; background: #f0f0f0; border-radius: 4px; overflow: hidden; position: relative; }
|
|
.bar-fill { height: 100%; border-radius: 4px; transition: width 0.8s ease-out; display: flex; align-items: center; padding-left: 8px; color: #fff; font-size: 13px; font-weight: 600; }
|
|
.bar-score { width: 40px; text-align: left; font-size: 14px; font-weight: 700; }
|
|
|
|
.interpretation { margin: 24px 0; padding: 20px; background: var(--gold-light); border-radius: 8px; border-left: 4px solid var(--gold); }
|
|
.interpretation h4 { font-size: 16px; color: var(--primary); margin-bottom: 8px; }
|
|
.interpretation p { font-size: 14px; color: var(--text); line-height: 1.7; }
|
|
|
|
.btn-primary { display: block; margin: 24px auto 0; padding: 14px 40px; background: linear-gradient(135deg, var(--primary), var(--primary-light)); color: #fff; border: none; border-radius: 8px; font-size: 16px; font-weight: 700; cursor: pointer; transition: all 0.2s; }
|
|
.btn-primary:hover { transform: translateY(-1px); box-shadow: 0 4px 12px rgba(26,58,92,0.3); }
|
|
.btn-primary:disabled { opacity: 0.5; cursor: not-allowed; }
|
|
|
|
.warning-box { background: #fff3cd; border: 1px solid #ffc107; border-radius: 8px; padding: 16px 20px; margin: 20px 0; font-size: 14px; color: #856404; }
|
|
.warning-box strong { color: #664d03; }
|
|
|
|
.footer { text-align: center; padding: 40px 20px; color: var(--text-light); font-size: 13px; }
|
|
|
|
/* History Panel */
|
|
.history-panel { background: var(--card); border-radius: 12px; padding: 28px; margin: 20px 0; box-shadow: 0 1px 3px rgba(0,0,0,0.08); }
|
|
.history-panel h3 { font-size: 18px; color: var(--primary); margin-bottom: 16px; display: flex; align-items: center; gap: 8px; }
|
|
.history-list { display: flex; flex-direction: column; gap: 8px; }
|
|
.history-item { display: flex; align-items: center; justify-content: space-between; padding: 14px 16px; border: 2px solid var(--border); border-radius: 8px; cursor: pointer; transition: all 0.15s; gap: 12px; flex-wrap: wrap; }
|
|
.history-item:hover { border-color: var(--primary-light); background: #f8fafc; }
|
|
.history-item.active { border-color: var(--primary); background: #ebf4ff; }
|
|
.history-item .hi-code { font-size: 20px; font-weight: 900; color: var(--primary); letter-spacing: 2px; }
|
|
.history-item .hi-time { font-size: 13px; color: var(--text-light); }
|
|
.history-item .hi-current { font-size: 12px; color: var(--success); font-weight: 600; }
|
|
.history-empty { text-align: center; color: var(--text-light); font-size: 14px; padding: 24px 0; }
|
|
.hi-delete { display: inline-flex; align-items: center; justify-content: center; width: 28px; height: 28px; border: none; border-radius: 50%; background: transparent; color: var(--text-light); font-size: 14px; cursor: pointer; transition: all 0.15s; flex-shrink: 0; margin-left: auto; }
|
|
.hi-delete:hover { background: #fee2e2; color: #dc2626; }
|
|
.history-footer { text-align: center; margin-top: 16px; padding-top: 16px; border-top: 1px solid var(--border); }
|
|
.btn-clear-history { display: inline-block; padding: 8px 24px; background: #fff; color: #dc2626; border: 1px solid #fecaca; border-radius: 6px; font-size: 13px; cursor: pointer; transition: all 0.2s; }
|
|
.btn-clear-history:hover { background: #fef2f2; border-color: #dc2626; }
|
|
.btn-reset { display: inline-block; padding: 10px 28px; background: #fff; color: var(--primary); border: 2px solid var(--primary); border-radius: 8px; font-size: 14px; font-weight: 600; cursor: pointer; transition: all 0.2s; }
|
|
.btn-reset:hover { background: var(--primary); color: #fff; }
|
|
.result-timestamp { text-align: center; font-size: 13px; color: var(--text-light); margin-top: -16px; margin-bottom: 16px; }
|
|
.btn-actions { display: flex; gap: 12px; justify-content: center; flex-wrap: wrap; margin-top: 24px; }
|
|
|
|
/* Dimension color classes */
|
|
.code-R { background: var(--accent-r); }
|
|
.code-I { background: var(--accent-i); }
|
|
.code-A { background: var(--accent-a); }
|
|
.code-S { background: var(--accent-s); }
|
|
.code-E { background: var(--accent-e); }
|
|
.code-C { background: var(--accent-c); }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
|
|
<div class="header">
|
|
<h1>🔬 霍兰德职业兴趣测试</h1>
|
|
<p class="subtitle">Holland RIASEC · 自评版 · 60题</p>
|
|
<span class="badge">全程约10分钟</span>
|
|
<span class="badge" style="background:#1a3a5c;color:#c9a84c;border:1px solid #c9a84c;margin-left:6px;">👤 天恒专属</span>
|
|
</div>
|
|
|
|
<div class="container">
|
|
|
|
<div class="intro-card">
|
|
<h3>📖 这是什么?</h3>
|
|
<p>霍兰德(John Holland)是美国心理学家,他发现人适合的职业和兴趣类型强相关。他把所有人分成 <strong>6种类型</strong>(RIASEC),每个人通常是其中2~3种类型的组合。</p>
|
|
</div>
|
|
|
|
<div class="intro-card">
|
|
<h3>📝 怎么填?</h3>
|
|
<p>下面有60个活动描述。对每个描述,请按<strong>你的真实感受</strong>选择:</p>
|
|
<p>🔴 <strong>不喜欢</strong> = 0分 | 🟡 <strong>有点喜欢</strong> = 1分 | 🟢 <strong>非常喜欢</strong> = 2分</p>
|
|
<p style="margin-top:8px;">💡 <strong>不是"你会不会做"而是"你想不想做"</strong>——假设你现在就会这些技能,你愿意做吗?</p>
|
|
</div>
|
|
|
|
<div id="dimNav" class="dim-nav" style="display:none;"></div>
|
|
|
|
<div class="progress-wrap">
|
|
<div class="progress-bar"><div id="progressFill" class="progress-fill" style="width:0%"></div></div>
|
|
<div class="progress-text" id="progressText">已答 0 / 60 题</div>
|
|
</div>
|
|
|
|
<div id="questions"></div>
|
|
|
|
<div class="section" id="submitSection" style="text-align:center;">
|
|
<button id="btnSubmit" class="btn-primary" disabled onclick="calculateResults()" style="margin-top:0;">📊 查看结果</button>
|
|
</div>
|
|
|
|
<div id="results" class="results">
|
|
<h2>🎯 你的霍兰德职业兴趣结果</h2>
|
|
<div class="result-timestamp" id="resultTimestamp"></div>
|
|
<div class="result-code">
|
|
<div class="code-letters" id="resultCode"></div>
|
|
<div class="code-label">三字母代码 · Holland Code</div>
|
|
</div>
|
|
<div id="barChart" class="bar-chart"></div>
|
|
<div id="interpretation" class="interpretation"></div>
|
|
<div class="btn-actions">
|
|
<button class="btn-reset" onclick="resetTest()">🔄 再做一次</button>
|
|
<button class="btn-reset" onclick="submitToServer()" style="border-color:var(--success);color:var(--success);">📤 提交结果到服务器</button>
|
|
<button class="btn-reset" onclick="exportCurrentResult()" style="border-color:var(--accent-i);color:var(--accent-i);">📥 导出当前结果</button>
|
|
<button class="btn-reset" onclick="exportAllHistory()" style="border-color:var(--accent-a);color:var(--accent-a);">📦 导出全部历史</button>
|
|
</div>
|
|
<div class="warning-box">
|
|
<strong>⚠️ 重要说明:</strong>本测试为自评简化版,用于快速了解兴趣倾向。结果仅作参考,不代表最终职业选择。完整60题的科学测评建议配合专业咨询师解读。
|
|
</div>
|
|
</div>
|
|
|
|
<div id="historyPanel" class="history-panel" style="display:none;">
|
|
<h3>📋 测试历史记录</h3>
|
|
<div id="historyList" class="history-list"></div>
|
|
</div>
|
|
|
|
</div>
|
|
|
|
<div class="footer">
|
|
<p>基于 John Holland RIASEC 理论编制 | 天恒专属 · 高考志愿规划工具</p>
|
|
</div>
|
|
|
|
<script>
|
|
// ========== Holland RIASEC Test Data ==========
|
|
const dimensions = [
|
|
{
|
|
code: 'R', name: '现实型', full: 'Realistic',
|
|
color: '#e53e3e',
|
|
desc: '动手操作型 — 喜欢摆弄机械、动手建造、户外活动',
|
|
items: [
|
|
'修理坏掉的电器或玩具',
|
|
'组装家具、模型或乐高',
|
|
'操作机器或电动工具',
|
|
'户外体力劳动(如搬东西、整理院子)',
|
|
'搭建或建造东西(如木工、搭棚子)',
|
|
'开车、骑车或操控机械设备',
|
|
'养花、种菜或打理植物',
|
|
'做手工(木工、金工、陶艺等)',
|
|
'调试设备或仪器',
|
|
'参加体育训练或比赛'
|
|
]
|
|
},
|
|
{
|
|
code: 'I', name: '研究型', full: 'Investigative',
|
|
color: '#3182ce',
|
|
desc: '研究思考型 — 喜欢分析问题、做实验、探索原理',
|
|
items: [
|
|
'解数学题或逻辑谜题',
|
|
'做科学实验(化学、物理、生物)',
|
|
'深入钻研某个感兴趣的话题',
|
|
'分析数据找规律(如统计成绩、对比数据)',
|
|
'读科普书籍或科技文章',
|
|
'用望远镜看星星、研究天文',
|
|
'玩解谜游戏或推理题',
|
|
'研究电脑或手机的工作原理',
|
|
'读研究报告或学术文章',
|
|
'独立思考一个复杂问题直到弄明白'
|
|
]
|
|
},
|
|
{
|
|
code: 'A', name: '艺术型', full: 'Artistic',
|
|
color: '#805ad5',
|
|
desc: '艺术创意型 — 喜欢创作、表达、设计、不被规则束缚',
|
|
items: [
|
|
'画画、素描或设计图案',
|
|
'写故事、诗歌或小说',
|
|
'演奏乐器或唱歌',
|
|
'拍照或拍视频(如剪Vlog、修图)',
|
|
'设计海报、Logo或宣传物料',
|
|
'布置房间、改变装饰风格',
|
|
'看画展、艺术展或设计展',
|
|
'自己设计衣服或搭配造型',
|
|
'创作音乐或编曲',
|
|
'参加表演、戏剧或即兴创作'
|
|
]
|
|
},
|
|
{
|
|
code: 'S', name: '社会型', full: 'Social',
|
|
color: '#38a169',
|
|
desc: '社会服务型 — 喜欢帮助别人、团队合作、与人打交道',
|
|
items: [
|
|
'帮同学讲解不会的题目',
|
|
'照顾生病或受伤的人',
|
|
'组织班级活动或朋友聚会',
|
|
'倾听朋友的烦恼并给予安慰',
|
|
'参加志愿者或公益活动',
|
|
'教小孩学习或带小孩玩',
|
|
'调解同学之间的矛盾',
|
|
'陪长辈或老人聊天',
|
|
'参加社团活动并积极参与',
|
|
'关心和帮助需要帮助的人'
|
|
]
|
|
},
|
|
{
|
|
code: 'E', name: '企业型', full: 'Enterprising',
|
|
color: '#dd6b20',
|
|
desc: '企业领导型 — 喜欢领导、说服、竞争、追求目标',
|
|
items: [
|
|
'在班级或社团担任负责人',
|
|
'说服别人接受你的观点或方案',
|
|
'组织或策划一场活动(如晚会、比赛)',
|
|
'卖东西或推销产品',
|
|
'在众人面前演讲或汇报',
|
|
'制定计划并推动团队执行',
|
|
'和别人谈判或讨价还价',
|
|
'尝试做小生意或创业想法',
|
|
'参加辩论赛或竞赛',
|
|
'带领团队完成一个有挑战的目标'
|
|
]
|
|
},
|
|
{
|
|
code: 'C', name: '常规型', full: 'Conventional',
|
|
color: '#718096',
|
|
desc: '常规有序型 — 喜欢有条理、按规则、精确细致的工作',
|
|
items: [
|
|
'整理文件或资料使其井井有条',
|
|
'制作表格、清单或日程安排',
|
|
'核对账目、数据或找错误',
|
|
'严格按照流程或步骤办事',
|
|
'管理日程、提醒和待办事项',
|
|
'记录和整理课堂笔记',
|
|
'分类整理物品(如图书、收藏品)',
|
|
'填写表格或制作报表',
|
|
'遵守时间表和计划不迟到',
|
|
'归档和管理文档使其易于查找'
|
|
]
|
|
}
|
|
];
|
|
|
|
// ========== State ==========
|
|
const scores = {}; // { 'R0': 0, 'R1': 1, ... }
|
|
dimensions.forEach(d => { for (let i=0; i<10; i++) scores[d.code + i] = -1; });
|
|
|
|
// ========== Render ==========
|
|
function renderAll() {
|
|
renderNav();
|
|
renderQuestions();
|
|
updateProgress();
|
|
}
|
|
|
|
function renderNav() {
|
|
const nav = document.getElementById('dimNav');
|
|
nav.innerHTML = dimensions.map(d => {
|
|
const answered = getDimAnswered(d.code);
|
|
const allDone = answered === 10;
|
|
return `<button class="${allDone ? 'done' : ''}" onclick="scrollToDim('${d.code}')">
|
|
<span class="dim-code code-${d.code}" style="display:inline-flex;width:24px;height:24px;font-size:12px">${d.code}</span> ${d.name}
|
|
</button>`;
|
|
}).join('');
|
|
nav.style.display = 'flex';
|
|
}
|
|
|
|
function renderQuestions() {
|
|
const container = document.getElementById('questions');
|
|
container.innerHTML = dimensions.map(d => `
|
|
<div class="section" id="dim-${d.code}">
|
|
<h2>
|
|
<span class="dim-code code-${d.code}">${d.code}</span>
|
|
${d.name}型 <span style="font-size:14px;color:var(--text-light);font-weight:400;">— ${d.desc}</span>
|
|
</h2>
|
|
${d.items.map((item, i) => {
|
|
const key = d.code + i;
|
|
const val = scores[key];
|
|
return `<div class="question">
|
|
<span class="q-text"><span class="q-num">${i+1}.</span>${item}</span>
|
|
<div class="rating" id="rating-${key}">
|
|
<button class="${val===0?'sel-zero':''}" onclick="setScore('${key}',0)">不喜欢</button>
|
|
<button class="${val===1?'sel-one':''}" onclick="setScore('${key}',1)">有点喜欢</button>
|
|
<button class="${val===2?'sel-two':''}" onclick="setScore('${key}',2)">非常喜欢</button>
|
|
</div>
|
|
</div>`;
|
|
}).join('')}
|
|
</div>
|
|
`).join('');
|
|
}
|
|
|
|
function setScore(key, val) {
|
|
scores[key] = val;
|
|
// Update button styles
|
|
const rating = document.getElementById('rating-' + key);
|
|
if (rating) {
|
|
rating.querySelectorAll('button').forEach((btn, i) => {
|
|
btn.className = i === val ? (i === 0 ? 'sel-zero' : i === 1 ? 'sel-one' : 'sel-two') : '';
|
|
});
|
|
}
|
|
updateProgress();
|
|
renderNav();
|
|
}
|
|
|
|
function getDimAnswered(code) {
|
|
let count = 0;
|
|
for (let i = 0; i < 10; i++) if (scores[code + i] >= 0) count++;
|
|
return count;
|
|
}
|
|
|
|
function updateProgress() {
|
|
let total = 0, answered = 0;
|
|
dimensions.forEach(d => {
|
|
for (let i = 0; i < 10; i++) { total++; if (scores[d.code + i] >= 0) answered++; }
|
|
});
|
|
const pct = Math.round((answered / total) * 100);
|
|
document.getElementById('progressFill').style.width = pct + '%';
|
|
document.getElementById('progressText').textContent = `已答 ${answered} / ${total} 题 (${pct}%)`;
|
|
document.getElementById('btnSubmit').disabled = answered < total;
|
|
}
|
|
|
|
function scrollToDim(code) {
|
|
document.getElementById('dim-' + code).scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
}
|
|
|
|
// ========== Calculate & Display ==========
|
|
function calculateResults() {
|
|
// Compute raw scores
|
|
const dimScores = dimensions.map(d => {
|
|
let sum = 0;
|
|
for (let i = 0; i < 10; i++) sum += scores[d.code + i];
|
|
return { code: d.code, name: d.name, full: d.full, desc: d.desc, color: d.color, score: sum };
|
|
});
|
|
|
|
// Sort descending
|
|
dimScores.sort((a, b) => b.score - a.score);
|
|
|
|
// Top 3 Holland code
|
|
const top3 = dimScores.slice(0, 3);
|
|
const code = top3.map(d => d.code).join('');
|
|
|
|
// Generate timestamp
|
|
const now = new Date();
|
|
const timestamp = now.toLocaleString('zh-CN', { year:'numeric', month:'2-digit', day:'2-digit', hour:'2-digit', minute:'2-digit', second:'2-digit', hour12:false });
|
|
|
|
// Display result code & timestamp
|
|
document.getElementById('resultCode').textContent = code;
|
|
document.getElementById('resultTimestamp').textContent = '⏰ 测试时间:' + timestamp;
|
|
|
|
// Bar chart
|
|
const maxScore = Math.max(...dimScores.map(d => d.score), 1);
|
|
document.getElementById('barChart').innerHTML = dimScores.map((d, idx) => {
|
|
const pct = Math.round((d.score / maxScore) * 100);
|
|
const isTop = idx < 3;
|
|
return `<div class="bar-row">
|
|
<span class="bar-label" style="color:${d.color};font-weight:700;">
|
|
<span style="background:${d.color};color:#fff;padding:2px 8px;border-radius:4px;font-size:12px;margin-right:4px;">${d.code}</span> ${d.name}
|
|
</span>
|
|
<div class="bar-track">
|
|
<div class="bar-fill" style="width:${pct}%;background:${d.color};${isTop?'':'opacity:0.6;'}">
|
|
${pct > 20 ? d.score + '分' : ''}
|
|
</div>
|
|
</div>
|
|
<span class="bar-score" style="color:${d.color};">${d.score}</span>
|
|
</div>`;
|
|
}).join('');
|
|
|
|
// Interpretation
|
|
const interpretations = {
|
|
R: { traits: '动手能力强、务实、直接', careers: '工程师、技师、飞行员、农业技术、建筑、机械制造、运动教练、电子维修', study: '工科(机械/电气/土木/航空航天)、体育、农学、职业技术教育' },
|
|
I: { traits: '善于分析、好奇心强、独立', careers: '科学家、医生、程序员、数据分析师、研究员、药剂师、数学老师、工程师', study: '理科(数学/物理/化学/生物)、医学、计算机科学、心理学、统计学' },
|
|
A: { traits: '有创造力、善于表达、敏感', careers: '设计师、作家、音乐家、摄影师、导演、插画师、UI设计师、建筑设计师', study: '设计学、艺术学、建筑学、文学、新闻传播、数字媒体' },
|
|
S: { traits: '乐于助人、善解人意、有耐心', careers: '教师、心理咨询师、社工、护士、医生、HR、培训师、导游', study: '教育学、心理学、医学、社会工作、人力资源管理' },
|
|
E: { traits: '自信、有说服力、目标导向', careers: '企业家、销售经理、律师、政治家、房产经纪人、市场总监、投资人', study: '工商管理、经济学、法学、市场营销、金融学' },
|
|
C: { traits: '有条理、细心、可靠', careers: '会计师、审计师、银行职员、行政人员、数据录入员、图书管理员、档案管理员', study: '会计学、财务管理、行政管理、图书情报、审计学' }
|
|
};
|
|
|
|
const primary = top3[0];
|
|
const interp = interpretations[primary.code];
|
|
document.getElementById('interpretation').innerHTML = `
|
|
<h4>📌 你的主导类型:${primary.code} — ${primary.name}型</h4>
|
|
<p><strong>性格特质:</strong>${interp.traits}</p>
|
|
<p><strong>适合职业方向:</strong>${interp.careers}</p>
|
|
<p><strong>匹配专业大类:</strong>${interp.study}</p>
|
|
<hr style="margin:16px 0;border-color:#ddd;">
|
|
<p><strong>完整三字母代码:</strong><span style="font-size:20px;font-weight:900;color:var(--primary);">${code}</span> — 表示你的兴趣类型是 ${top3.map(d=>d.name+'型').join(' + ')} 的组合。</p>
|
|
<p style="margin-top:8px;font-size:13px;color:var(--text-light);">大多数人的兴趣是2~3种类型的组合,极少有人是单一类型。你的前三类型组合在一起,才构成你独特的职业兴趣画像。</p>
|
|
`;
|
|
|
|
// Show results
|
|
document.getElementById('results').classList.add('show');
|
|
document.getElementById('results').scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
|
|
// Save result to localStorage
|
|
const resultData = { code, dimScores, timestamp: now.toISOString(), timestampFormatted: timestamp };
|
|
saveResult(resultData);
|
|
renderHistory();
|
|
}
|
|
|
|
// ========== localStorage Persistence ==========
|
|
const STORAGE_KEY = 'holland_test_history';
|
|
|
|
function saveResult(resultData) {
|
|
let history = [];
|
|
try {
|
|
history = JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]');
|
|
} catch(e) { history = []; }
|
|
history.push(resultData);
|
|
// Keep max 20 entries
|
|
if (history.length > 20) history = history.slice(-20);
|
|
try {
|
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(history));
|
|
} catch(e) { /* quota exceeded, silently fail */ }
|
|
}
|
|
|
|
function loadHistory() {
|
|
try {
|
|
return JSON.parse(localStorage.getItem(STORAGE_KEY) || '[]');
|
|
} catch(e) { return []; }
|
|
}
|
|
|
|
function renderHistory() {
|
|
const history = loadHistory();
|
|
const panel = document.getElementById('historyPanel');
|
|
const list = document.getElementById('historyList');
|
|
|
|
if (history.length === 0) {
|
|
list.innerHTML = '<div class="history-empty">暂无历史记录,完成一次测试后会自动保存</div>';
|
|
} else {
|
|
list.innerHTML = history.map((h, idx) => {
|
|
const isLatest = idx === history.length - 1;
|
|
return `<div class="history-item${isLatest ? ' active' : ''}" onclick="showHistoryResult(${idx})">
|
|
<span class="hi-code">${h.code}</span>
|
|
<span class="hi-time">${h.timestampFormatted}</span>
|
|
${isLatest ? '<span class="hi-current">最新</span>' : ''}
|
|
<button class="hi-delete" title="删除此条记录" onclick="event.stopPropagation();deleteHistoryItem(${idx});">✕</button>
|
|
</div>`;
|
|
}).reverse().join(''); // newest first
|
|
list.innerHTML += '<div class="history-footer"><button class="btn-clear-history" onclick="clearAllHistory()">🗑️ 清空全部历史</button></div>';
|
|
}
|
|
panel.style.display = 'block';
|
|
}
|
|
|
|
function deleteHistoryItem(idx) {
|
|
if (!confirm('确定删除这条测试记录吗?')) return;
|
|
let history = loadHistory();
|
|
if (idx < 0 || idx >= history.length) return;
|
|
history.splice(idx, 1);
|
|
try {
|
|
localStorage.setItem(STORAGE_KEY, JSON.stringify(history));
|
|
} catch(e) { /* quota exceeded, silently fail */ }
|
|
renderHistory();
|
|
// If no history left, hide the panel
|
|
if (history.length === 0) {
|
|
document.getElementById('historyPanel').style.display = 'none';
|
|
}
|
|
}
|
|
|
|
function clearAllHistory() {
|
|
if (!confirm('确定清空全部测试历史记录吗?此操作不可恢复。')) return;
|
|
try {
|
|
localStorage.removeItem(STORAGE_KEY);
|
|
} catch(e) { /* silently fail */ }
|
|
document.getElementById('historyPanel').style.display = 'none';
|
|
document.getElementById('historyList').innerHTML = '';
|
|
}
|
|
|
|
function showHistoryResult(idx) {
|
|
const history = loadHistory();
|
|
if (idx < 0 || idx >= history.length) return;
|
|
const h = history[idx];
|
|
|
|
// Highlight active item
|
|
document.querySelectorAll('.history-item').forEach((el, i) => {
|
|
el.classList.toggle('active', i === (history.length - 1 - idx)); // reversed display
|
|
});
|
|
|
|
// Re-display result (same display logic as calculateResults, but from stored data)
|
|
document.getElementById('resultCode').textContent = h.code;
|
|
document.getElementById('resultTimestamp').textContent = '⏰ 测试时间:' + h.timestampFormatted;
|
|
|
|
const maxScore = Math.max(...h.dimScores.map(d => d.score), 1);
|
|
document.getElementById('barChart').innerHTML = h.dimScores.map((d, idx2) => {
|
|
const pct = Math.round((d.score / maxScore) * 100);
|
|
const isTop = idx2 < 3;
|
|
return `<div class="bar-row">
|
|
<span class="bar-label" style="color:${d.color};font-weight:700;">
|
|
<span style="background:${d.color};color:#fff;padding:2px 8px;border-radius:4px;font-size:12px;margin-right:4px;">${d.code}</span> ${d.name}
|
|
</span>
|
|
<div class="bar-track">
|
|
<div class="bar-fill" style="width:${pct}%;background:${d.color};${isTop?'':'opacity:0.6;'}">
|
|
${pct > 20 ? d.score + '分' : ''}
|
|
</div>
|
|
</div>
|
|
<span class="bar-score" style="color:${d.color};">${d.score}</span>
|
|
</div>`;
|
|
}).join('');
|
|
|
|
const interpretations = {
|
|
R: { traits: '动手能力强、务实、直接', careers: '工程师、技师、飞行员、农业技术、建筑、机械制造、运动教练、电子维修', study: '工科(机械/电气/土木/航空航天)、体育、农学、职业技术教育' },
|
|
I: { traits: '善于分析、好奇心强、独立', careers: '科学家、医生、程序员、数据分析师、研究员、药剂师、数学老师、工程师', study: '理科(数学/物理/化学/生物)、医学、计算机科学、心理学、统计学' },
|
|
A: { traits: '有创造力、善于表达、敏感', careers: '设计师、作家、音乐家、摄影师、导演、插画师、UI设计师、建筑设计师', study: '设计学、艺术学、建筑学、文学、新闻传播、数字媒体' },
|
|
S: { traits: '乐于助人、善解人意、有耐心', careers: '教师、心理咨询师、社工、护士、医生、HR、培训师、导游', study: '教育学、心理学、医学、社会工作、人力资源管理' },
|
|
E: { traits: '自信、有说服力、目标导向', careers: '企业家、销售经理、律师、政治家、房产经纪人、市场总监、投资人', study: '工商管理、经济学、法学、市场营销、金融学' },
|
|
C: { traits: '有条理、细心、可靠', careers: '会计师、审计师、银行职员、行政人员、数据录入员、图书管理员、档案管理员', study: '会计学、财务管理、行政管理、图书情报、审计学' }
|
|
};
|
|
const interp = interpretations[h.dimScores[0].code];
|
|
document.getElementById('interpretation').innerHTML = `
|
|
<h4>📌 你的主导类型:${h.dimScores[0].code} — ${h.dimScores[0].name}型</h4>
|
|
<p><strong>性格特质:</strong>${interp.traits}</p>
|
|
<p><strong>适合职业方向:</strong>${interp.careers}</p>
|
|
<p><strong>匹配专业大类:</strong>${interp.study}</p>
|
|
<hr style="margin:16px 0;border-color:#ddd;">
|
|
<p><strong>完整三字母代码:</strong><span style="font-size:20px;font-weight:900;color:var(--primary);">${h.code}</span> — 表示你的兴趣类型是 ${h.dimScores.slice(0,3).map(d=>d.name+'型').join(' + ')} 的组合。</p>
|
|
<p style="margin-top:8px;font-size:13px;color:var(--text-light);">大多数人的兴趣是2~3种类型的组合,极少有人是单一类型。你的前三类型组合在一起,才构成你独特的职业兴趣画像。</p>
|
|
`;
|
|
|
|
document.getElementById('results').classList.add('show');
|
|
document.getElementById('results').scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
}
|
|
|
|
function resetTest() {
|
|
if (!confirm('确定要重新测试吗?当前结果已自动保存到历史记录中,不会丢失。')) return;
|
|
// Reset all scores
|
|
dimensions.forEach(d => { for (let i = 0; i < 10; i++) scores[d.code + i] = -1; });
|
|
// Hide results
|
|
document.getElementById('results').classList.remove('show');
|
|
document.getElementById('resultCode').textContent = '';
|
|
document.getElementById('resultTimestamp').textContent = '';
|
|
document.getElementById('barChart').innerHTML = '';
|
|
document.getElementById('interpretation').innerHTML = '';
|
|
// Re-render questions
|
|
renderAll();
|
|
// Scroll to top
|
|
window.scrollTo({ top: 0, behavior: 'smooth' });
|
|
}
|
|
|
|
// ========== 提交到服务器 ==========
|
|
async function submitToServer() {
|
|
const code = document.getElementById('resultCode').textContent;
|
|
if (!code) { alert('请先完成测试再提交'); return; }
|
|
|
|
const dimScores = dimensions.map(d => {
|
|
let sum = 0;
|
|
for (let i = 0; i < 10; i++) sum += scores[d.code + i];
|
|
return { code: d.code, name: d.name, score: sum };
|
|
}).sort((a, b) => b.score - a.score);
|
|
|
|
try {
|
|
const resp = await fetch('/api/standalone/submit', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
test_name: 'holland',
|
|
user_name: '天恒',
|
|
result: code,
|
|
full_data: { code, dimScores }
|
|
})
|
|
});
|
|
const result = await resp.json();
|
|
if (result.success) {
|
|
alert('✅ 霍兰德测试结果已成功提交到服务器!\n可在首页「问卷完成情况」查看。');
|
|
} else {
|
|
alert('提交失败:' + (result.error || '未知错误'));
|
|
}
|
|
} catch(e) {
|
|
alert('提交失败:无法连接服务器。请确保通过 NAS 地址访问本页面。');
|
|
}
|
|
}
|
|
|
|
// ========== Export ==========
|
|
function exportCurrentResult() {
|
|
const history = loadHistory();
|
|
if (history.length === 0) { alert('暂无测试结果可导出'); return; }
|
|
const last = history[history.length - 1];
|
|
const dateStr = new Date().toISOString().slice(0, 10);
|
|
const blob = new Blob([JSON.stringify(last, null, 2)], { type: 'application/json' });
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = `霍兰德测试结果_${dateStr}.json`;
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
document.body.removeChild(a);
|
|
URL.revokeObjectURL(url);
|
|
}
|
|
|
|
function exportAllHistory() {
|
|
const history = loadHistory();
|
|
if (history.length === 0) { alert('暂无历史记录可导出'); return; }
|
|
const dateStr = new Date().toISOString().slice(0, 10);
|
|
const blob = new Blob([JSON.stringify(history, null, 2)], { type: 'application/json' });
|
|
const url = URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = `霍兰德测试全部历史_${dateStr}.json`;
|
|
document.body.appendChild(a);
|
|
a.click();
|
|
document.body.removeChild(a);
|
|
URL.revokeObjectURL(url);
|
|
}
|
|
|
|
// ========== Init ==========
|
|
renderAll();
|
|
// Load and show history on page load if exists
|
|
(function() {
|
|
const history = loadHistory();
|
|
if (history.length > 0) {
|
|
renderHistory();
|
|
}
|
|
})();
|
|
</script>
|
|
</body>
|
|
</html>
|