v1.0.0 高考志愿门户完整修复
变更摘要: - [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 仓库初始化
This commit is contained in:
@@ -0,0 +1,645 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>MBTI性格测试 · 自评版</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;
|
||||
--ei-color: #805ad5;
|
||||
--sn-color: #3182ce;
|
||||
--tf-color: #dd6b20;
|
||||
--jp-color: #38a169;
|
||||
}
|
||||
* { 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 { 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 { max-width: 720px; margin: 0 auto; padding: 20px; }
|
||||
|
||||
.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; }
|
||||
|
||||
.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 { 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; margin-bottom: 4px; display: flex; align-items: center; gap: 10px; }
|
||||
.section .dim-badge { display: inline-flex; align-items: center; justify-content: center; min-width: 80px; height: 36px; border-radius: 20px; font-weight: 700; font-size: 16px; color: #fff; padding: 0 14px; flex-shrink: 0; }
|
||||
.section .dim-desc { font-size: 14px; color: var(--text-light); margin: 8px 0 20px; }
|
||||
|
||||
.question { padding: 16px 0; border-bottom: 1px solid var(--border); }
|
||||
.question:last-child { border-bottom: none; }
|
||||
.question .q-num { font-weight: 700; color: var(--primary); margin-right: 6px; font-size: 14px; }
|
||||
.question .q-text { font-size: 15px; font-weight: 600; margin-bottom: 10px; }
|
||||
|
||||
.choice-group { display: flex; flex-direction: column; gap: 6px; }
|
||||
.choice { display: flex; align-items: center; padding: 10px 14px; border: 2px solid var(--border); border-radius: 8px; cursor: pointer; transition: all 0.15s; gap: 10px; }
|
||||
.choice:hover { border-color: var(--primary-light); background: #f8fafc; }
|
||||
.choice.selected-a { border-color: #3182ce; background: #ebf8ff; }
|
||||
.choice.selected-b { border-color: #dd6b20; background: #fffaf0; }
|
||||
.choice .radio { width: 20px; height: 20px; border-radius: 50%; border: 2px solid var(--border); display: flex; align-items: center; justify-content: center; flex-shrink: 0; font-size: 12px; }
|
||||
.choice.selected-a .radio { border-color: #3182ce; background: #3182ce; color: #fff; }
|
||||
.choice.selected-b .radio { border-color: #dd6b20; background: #dd6b20; color: #fff; }
|
||||
.choice .opt-label { font-size: 14px; line-height: 1.5; }
|
||||
|
||||
.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 { 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-type { text-align: center; margin: 20px 0; }
|
||||
.result-type .type-letters { font-size: 56px; font-weight: 900; letter-spacing: 6px; background: linear-gradient(135deg, var(--primary), var(--gold)); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; }
|
||||
.result-type .type-label { font-size: 15px; color: var(--text-light); margin-top: 4px; }
|
||||
|
||||
.dim-results { margin: 24px 0; display: grid; grid-template-columns: 1fr 1fr; gap: 16px; }
|
||||
@media (max-width: 500px) { .dim-results { grid-template-columns: 1fr; } }
|
||||
.dim-card { padding: 16px; border-radius: 8px; text-align: center; border: 2px solid var(--border); }
|
||||
.dim-card .dim-name { font-size: 12px; color: var(--text-light); text-transform: uppercase; letter-spacing: 2px; }
|
||||
.dim-card .dim-result { font-size: 28px; font-weight: 900; margin: 6px 0; }
|
||||
.dim-card .dim-bar-wrap { height: 6px; background: #f0f0f0; border-radius: 3px; margin-top: 8px; overflow: hidden; }
|
||||
.dim-card .dim-bar-fill { height: 100%; border-radius: 3px; transition: width 1s ease-out; }
|
||||
|
||||
.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; margin-bottom: 6px; }
|
||||
|
||||
.type-traits { margin: 20px 0; padding: 20px; background: #f0f4ff; border-radius: 8px; }
|
||||
.type-traits h4 { font-size: 15px; color: var(--primary); margin-bottom: 8px; }
|
||||
.type-traits ul { padding-left: 20px; font-size: 14px; }
|
||||
.type-traits li { margin: 4px 0; }
|
||||
|
||||
.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-type { font-size: 20px; font-weight: 900; color: var(--primary); letter-spacing: 3px; }
|
||||
.history-item .hi-name { font-size: 14px; color: var(--text-light); }
|
||||
.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; }
|
||||
|
||||
.bg-ei { background: #f3e8ff; border-color: var(--ei-color); }
|
||||
.bg-sn { background: #e8f0ff; border-color: var(--sn-color); }
|
||||
.bg-tf { background: #fff4e8; border-color: var(--tf-color); }
|
||||
.bg-jp { background: #e8fff0; border-color: var(--jp-color); }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<div class="header">
|
||||
<h1>🧠 MBTI 性格测试</h1>
|
||||
<p class="subtitle">Myers-Briggs Type Indicator · 自评版 · 28题</p>
|
||||
<span class="badge">全程约8分钟</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>MBTI基于荣格心理学理论,把人的性格分成<strong>4个维度</strong>,组合成16种类型。它不判断"好坏",只描述"你是怎么样的人"。</p>
|
||||
</div>
|
||||
|
||||
<div class="intro-card">
|
||||
<h3>📝 怎么填?</h3>
|
||||
<p>下面有28道题,每题有 <strong>A</strong> 和 <strong>B</strong> 两个选项。请选择<strong>更接近你真实感受</strong>的那个——不是"应该选哪个",而是<strong>"你实际上更偏向哪边"</strong>。</p>
|
||||
<p style="margin-top:8px;">💡 如果两个都不完全符合,选<strong>相对更贴切</strong>的那个。尽量不要跳题。</p>
|
||||
</div>
|
||||
|
||||
<div class="intro-card" style="border-left-color: var(--gold); background: var(--gold-light);">
|
||||
<h3>4个维度速览</h3>
|
||||
<table style="width:100%;font-size:14px;border-collapse:collapse;">
|
||||
<tr><td style="padding:6px 0;"><strong>E 外向 vs I 内向</strong></td><td>你从哪获得能量?社交充电 vs 独处充电</td></tr>
|
||||
<tr><td style="padding:6px 0;"><strong>S 实感 vs N 直觉</strong></td><td>你如何接收信息?相信具体事实 vs 相信直觉规律</td></tr>
|
||||
<tr><td style="padding:6px 0;"><strong>T 思考 vs F 情感</strong></td><td>你如何做决定?靠逻辑分析 vs 靠价值观和感受</td></tr>
|
||||
<tr><td style="padding:6px 0;"><strong>J 判断 vs P 感知</strong></td><td>你如何安排生活?喜欢计划有序 vs 喜欢灵活开放</td></tr>
|
||||
</table>
|
||||
</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 / 28 题</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>🎯 你的MBTI性格类型</h2>
|
||||
<div class="result-timestamp" id="resultTimestamp"></div>
|
||||
<div class="result-type">
|
||||
<div class="type-letters" id="resultType"></div>
|
||||
<div class="type-label">四字母代码 · Personality Type</div>
|
||||
</div>
|
||||
<div id="dimDetails" class="dim-results"></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(--sn-color);color:var(--sn-color);">📥 导出当前结果</button>
|
||||
<button class="btn-reset" onclick="exportAllHistory()" style="border-color:var(--ei-color);color:var(--ei-color);">📦 导出全部历史</button>
|
||||
</div>
|
||||
<div class="warning-box">
|
||||
<strong>⚠️ 重要说明:</strong>本测试为自评简化版,用于快速了解性格倾向。MBTI类型会随年龄和经历变化,结果仅作参考,不应作为唯一决策依据。完整93题的科学测评建议配合专业认证施测师。
|
||||
</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>基于 Myers-Briggs Type Indicator 理论编制 | 天恒专属 · 高考志愿规划工具</p>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
// ========== MBTI Test Data ==========
|
||||
const dimensions = [
|
||||
{
|
||||
id: 'ei', codeA: 'E', codeB: 'I',
|
||||
name: '外向 E vs 内向 I',
|
||||
fullName: 'Extraversion vs Introversion',
|
||||
desc: '你从哪获得能量?',
|
||||
color: '#805ad5',
|
||||
bgClass: 'bg-ei',
|
||||
items: [
|
||||
{ a: '和一群朋友聚会时,我精力充沛、越玩越嗨', b: '我更享受独处或和一二知己深聊,人太多反而消耗' },
|
||||
{ a: '我喜欢先说出来再慢慢理清思路', b: '我喜欢先想清楚再开口,说出来的都是想好的' },
|
||||
{ a: '认识新朋友让我兴奋', b: '认识新朋友让我有点紧张' },
|
||||
{ a: '我更喜欢热闹活跃的环境', b: '我更喜欢安静、不被打扰的环境' },
|
||||
{ a: '在群体中我倾向于主动发起对话', b: '在群体中我倾向于等别人先开口' },
|
||||
{ a: '我的社交圈比较广,不同类型的朋友都有', b: '我有少数几个深交的朋友,已经足够' },
|
||||
{ a: '参加完集体活动后我精神焕发', b: '参加完集体活动后我需要独处恢复能量' }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'sn', codeA: 'S', codeB: 'N',
|
||||
name: '实感 S vs 直觉 N',
|
||||
fullName: 'Sensing vs Intuition',
|
||||
desc: '你如何接收和理解信息?',
|
||||
color: '#3182ce',
|
||||
bgClass: 'bg-sn',
|
||||
items: [
|
||||
{ a: '我更关注具体的细节和事实数据', b: '我更关注整体图景和未来可能性' },
|
||||
{ a: '我喜欢按部就班、有章可循的方法', b: '我喜欢尝试新方法、新思路,哪怕不一定成功' },
|
||||
{ a: '我相信眼见为实,需要看到证据', b: '我相信直觉和预感,哪怕暂时说不清为什么' },
|
||||
{ a: '我更看重实用的技能和技术', b: '我更着迷于抽象的理论和概念' },
|
||||
{ a: '我更多关注当下和过往的经验', b: '我更多关注未来和可能性' },
|
||||
{ a: '我喜欢有明确步骤和流程的任务', b: '我喜欢自由发挥和探索的任务' },
|
||||
{ a: '做熟悉的事让我感到踏实', b: '不断尝试新事物才让我觉得有意思' }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'tf', codeA: 'T', codeB: 'F',
|
||||
name: '思考 T vs 情感 F',
|
||||
fullName: 'Thinking vs Feeling',
|
||||
desc: '你如何做决定和判断?',
|
||||
color: '#dd6b20',
|
||||
bgClass: 'bg-tf',
|
||||
items: [
|
||||
{ a: '做决定时我更看重逻辑分析和客观事实', b: '做决定时我更看重价值观和对他人的影响' },
|
||||
{ a: '我认为公平公正比照顾个人感受更重要', b: '我认为理解和体谅比死守规则更重要' },
|
||||
{ a: '别人常说我比较理性、冷静', b: '别人常说我比较感性、有同理心' },
|
||||
{ a: '发现问题时我会直接指出来', b: '指出问题前我会先考虑会不会伤到对方' },
|
||||
{ a: '我更看重效率和结果', b: '我更看重过程和人际关系的和谐' },
|
||||
{ a: '给别人提意见或批评对我来说不太难', b: '批评别人让我感到很不舒服' },
|
||||
{ a: '我认为规则既然定了就该严格执行', b: '我认为规则可以根据实际情况灵活处理' }
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'jp', codeA: 'J', codeB: 'P',
|
||||
name: '判断 J vs 感知 P',
|
||||
fullName: 'Judging vs Perceiving',
|
||||
desc: '你如何安排生活和应对外部世界?',
|
||||
color: '#38a169',
|
||||
bgClass: 'bg-jp',
|
||||
items: [
|
||||
{ a: '我喜欢提前计划,什么事都安排好', b: '我喜欢随遇而安,灵活应对变化' },
|
||||
{ a: '提前完成任务让我感到轻松和满足', b: '截止日期前的压力反而让我效率最高' },
|
||||
{ a: '我喜欢井井有条、有规律的生活', b: '我享受即兴发挥和不被安排的自由' },
|
||||
{ a: '做出决定后我感觉如释重负', b: '我喜欢保持选择开放,不急着做决定' },
|
||||
{ a: '出去玩之前我喜欢做好攻略和计划', b: '走哪算哪,随性探索更有趣' },
|
||||
{ a: '我喜欢先把该做的事做完再放松', b: '有时候先放松一下再做事效率更高' },
|
||||
{ a: '有规律的作息让我状态稳定', b: '我根据状态和心情来安排时间' }
|
||||
]
|
||||
}
|
||||
];
|
||||
|
||||
// ========== State ==========
|
||||
const answers = {}; // { 'ei0': 'A', 'ei1': 'B', ... }
|
||||
dimensions.forEach(d => { for (let i = 0; i < 7; i++) answers[d.id + i] = null; });
|
||||
|
||||
// ========== Render ==========
|
||||
function renderAll() {
|
||||
renderNav();
|
||||
renderQuestions();
|
||||
updateProgress();
|
||||
}
|
||||
|
||||
function renderNav() {
|
||||
const nav = document.getElementById('dimNav');
|
||||
nav.innerHTML = dimensions.map(d => {
|
||||
const answered = getDimAnswered(d.id);
|
||||
const allDone = answered === 7;
|
||||
return `<button class="${allDone ? 'done' : ''}" onclick="scrollToDim('${d.id}')" style="border-color:${d.color};">
|
||||
${d.name}
|
||||
</button>`;
|
||||
}).join('');
|
||||
nav.style.display = 'flex';
|
||||
}
|
||||
|
||||
function renderQuestions() {
|
||||
const container = document.getElementById('questions');
|
||||
container.innerHTML = dimensions.map(d => `
|
||||
<div class="section ${d.bgClass}" id="dim-${d.id}" style="border-top: 3px solid ${d.color};">
|
||||
<h2>
|
||||
<span class="dim-badge" style="background:${d.color};">${d.name}</span>
|
||||
</h2>
|
||||
<p class="dim-desc">${d.desc} 共7题,每题选A或B</p>
|
||||
${d.items.map((item, i) => {
|
||||
const key = d.id + i;
|
||||
const val = answers[key];
|
||||
return `<div class="question">
|
||||
<div class="q-text"><span class="q-num">${i+1}.</span>请选择更接近你的选项:</div>
|
||||
<div class="choice-group">
|
||||
<div class="choice ${val==='A'?'selected-a':''}" onclick="setAnswer('${key}','A')" id="choice-${key}-A">
|
||||
<div class="radio">${val==='A'?'✓':''}</div>
|
||||
<span class="opt-label"><strong>A.</strong> ${item.a}</span>
|
||||
</div>
|
||||
<div class="choice ${val==='B'?'selected-b':''}" onclick="setAnswer('${key}','B')" id="choice-${key}-B">
|
||||
<div class="radio">${val==='B'?'✓':''}</div>
|
||||
<span class="opt-label"><strong>B.</strong> ${item.b}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>`;
|
||||
}).join('')}
|
||||
</div>
|
||||
`).join('');
|
||||
}
|
||||
|
||||
function setAnswer(key, val) {
|
||||
answers[key] = val;
|
||||
// Update choice styles
|
||||
const dimId = key.slice(0, 2);
|
||||
const idx = parseInt(key.slice(2));
|
||||
const choiceA = document.getElementById('choice-' + key + '-A');
|
||||
const choiceB = document.getElementById('choice-' + key + '-B');
|
||||
if (choiceA) choiceA.className = 'choice ' + (val === 'A' ? 'selected-a' : '');
|
||||
if (choiceB) choiceB.className = 'choice ' + (val === 'B' ? 'selected-b' : '');
|
||||
updateProgress();
|
||||
renderNav();
|
||||
}
|
||||
|
||||
function getDimAnswered(id) {
|
||||
let count = 0;
|
||||
for (let i = 0; i < 7; i++) if (answers[id + i] !== null) count++;
|
||||
return count;
|
||||
}
|
||||
|
||||
function updateProgress() {
|
||||
let total = 0, answered = 0;
|
||||
dimensions.forEach(d => {
|
||||
for (let i = 0; i < 7; i++) { total++; if (answers[d.id + i] !== null) 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(id) {
|
||||
document.getElementById('dim-' + id).scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
}
|
||||
|
||||
// ========== Calculate & Display ==========
|
||||
function calculateResults() {
|
||||
// Compute scores for each dichotomy
|
||||
const dimResults = dimensions.map(d => {
|
||||
let countA = 0;
|
||||
for (let i = 0; i < 7; i++) if (answers[d.id + i] === 'A') countA++;
|
||||
const scoreA = countA;
|
||||
const scoreB = 7 - countA;
|
||||
const result = countA >= 4 ? d.codeA : d.codeB;
|
||||
const pctA = Math.round((countA / 7) * 100);
|
||||
const certainty = (countA === 3 || countA === 4) ? '(边界型,两种倾向接近)' : '';
|
||||
return {
|
||||
id: d.id, name: d.name, codeA: d.codeA, codeB: d.codeB, result, scoreA, scoreB, pctA,
|
||||
color: d.color, certainty, bgClass: d.bgClass
|
||||
};
|
||||
});
|
||||
|
||||
// Full MBTI type
|
||||
const mbtiType = dimResults.map(d => d.result).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 type & timestamp
|
||||
document.getElementById('resultType').textContent = mbtiType;
|
||||
document.getElementById('resultTimestamp').textContent = '⏰ 测试时间:' + timestamp;
|
||||
displayMBTIResult(mbtiType, dimResults);
|
||||
|
||||
// Show results
|
||||
document.getElementById('results').classList.add('show');
|
||||
document.getElementById('results').scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
|
||||
// Save result to localStorage
|
||||
const resultData = { type: mbtiType, details: dimResults, timestamp: now.toISOString(), timestampFormatted: timestamp };
|
||||
saveResult(resultData);
|
||||
renderHistory();
|
||||
}
|
||||
|
||||
function displayMBTIResult(mbtiType, dimResults) {
|
||||
// Dimension detail cards
|
||||
document.getElementById('dimDetails').innerHTML = dimResults.map(d => `
|
||||
<div class="dim-card ${d.bgClass}">
|
||||
<div class="dim-name">${d.name}</div>
|
||||
<div class="dim-result" style="color:${d.color};">${d.result}</div>
|
||||
<div style="font-size:12px;color:var(--text-light);">
|
||||
${d.codeA} ${d.scoreA}/7 ← → ${d.codeB} ${d.scoreB}/7
|
||||
${d.certainty ? '<br>'+d.certainty : ''}
|
||||
</div>
|
||||
<div class="dim-bar-wrap">
|
||||
<div class="dim-bar-fill" style="width:${d.pctA}%;background:${d.color};border-radius:3px 0 0 3px;"></div>
|
||||
</div>
|
||||
</div>
|
||||
`).join('');
|
||||
|
||||
const typeDescriptions = {
|
||||
'INTJ': { name: '建筑师', traits: ['善于独立思考', '有战略眼光', '追求完美', '喜欢规划和系统'], study: '科研、工程、计算机、法学、经济学', note: '适合需要深度思考和独立研究的专业' },
|
||||
'INTP': { name: '逻辑学家', traits: ['好奇心极强', '善于分析', '喜欢理论探索', '不拘常规'], study: '数学、物理、计算机科学、哲学、心理学', note: '适合需要创新和理论深度的专业' },
|
||||
'ENTJ': { name: '指挥官', traits: ['天生领导者', '果断自信', '目标驱动', '善于组织'], study: '管理学、经济学、法学、工程管理、政治学', note: '适合需要领导力和战略眼光的专业' },
|
||||
'ENTP': { name: '辩论家', traits: ['思维敏捷', '喜欢挑战', '善于创新', '不喜欢重复'], study: '法学、新闻学、市场营销、计算机、设计学', note: '适合需要创造力和快速应变的专业' },
|
||||
'INFJ': { name: '提倡者', traits: ['理想主义', '洞察力强', '关心他人', '有使命感'], study: '心理学、教育学、医学、社会工作、文学', note: '适合需要深度共情和价值感的专业' },
|
||||
'INFP': { name: '调停者', traits: ['内心丰富', '忠于价值观', '富有创造力', '追求意义'], study: '文学、艺术学、心理学、哲学、设计学', note: '适合需要自我表达和意义感的专业' },
|
||||
'ENFJ': { name: '主人公', traits: ['魅力十足', '善解人意', '激励他人', '有责任感'], study: '教育学、人力资源管理、新闻传播、医学、公共管理', note: '适合需要感染力和组织力的专业' },
|
||||
'ENFP': { name: '竞选者', traits: ['热情洋溢', '想象力丰富', '善于社交', '追求新鲜感'], study: '新闻学、市场营销、设计学、心理学、文学', note: '适合需要创意和人际互动的专业' },
|
||||
'ISTJ': { name: '物流师', traits: ['务实可靠', '做事有条理', '遵守规则', '注重细节'], study: '会计学、审计学、工程管理、行政管理、法学', note: '适合需要严谨和规范的专业' },
|
||||
'ISFJ': { name: '守卫者', traits: ['细心周到', '有奉献精神', '踏实可靠', '重视传统'], study: '医学、护理学、教育学、社会工作、档案学', note: '适合需要耐心和细致的服务型专业' },
|
||||
'ESTJ': { name: '总经理', traits: ['执行力强', '组织有序', '务实高效', '喜欢管理'], study: '管理学、会计学、工程管理、法学、军事学', note: '适合需要执行力和管理能力的专业' },
|
||||
'ESFJ': { name: '执政官', traits: ['热心周到', '善于合作', '注重和谐', '乐于助人'], study: '教育学、护理学、人力资源、酒店管理、社会工作', note: '适合需要人际协调和服务的专业' },
|
||||
'ISTP': { name: '鉴赏家', traits: ['动手能力强', '冷静务实', '喜欢探索原理', '不拘小节'], study: '机械工程、计算机科学、电子信息、体育学、工艺美术', note: '适合需要实操和解决问题的专业' },
|
||||
'ISFP': { name: '探险家', traits: ['审美敏锐', '温和友善', '活在当下', '不喜欢冲突'], study: '设计学、艺术学、音乐学、园艺、护理学', note: '适合需要审美和动手创作的专业' },
|
||||
'ESTP': { name: '企业家', traits: ['行动派', '适应力强', '喜欢冒险', '善于应变'], study: '市场营销、体育学、管理学、法学、创业学', note: '适合需要快速反应和实战能力的专业' },
|
||||
'ESFP': { name: '表演者', traits: ['活力四射', '社交达人', '享受当下', '乐于助人'], study: '表演艺术、旅游管理、新闻传播、社会工作、体育教育', note: '适合需要表现力和人际互动的专业' }
|
||||
};
|
||||
|
||||
const t = typeDescriptions[mbtiType] || { name: '', traits: [], study: '', note: '' };
|
||||
const ei = dimResults[0], sn = dimResults[1], tf = dimResults[2], jp = dimResults[3];
|
||||
document.getElementById('interpretation').innerHTML = `
|
||||
<h4>📌 你的MBTI类型:${mbtiType} — ${t.name}</h4>
|
||||
<p><strong>性格特征:</strong>${t.traits.join('、')}</p>
|
||||
<p><strong>推荐专业方向:</strong>${t.study}</p>
|
||||
<p><strong>选择建议:</strong>${t.note}</p>
|
||||
<hr style="margin:16px 0;border-color:#ddd;">
|
||||
<p style="font-size:13px;color:var(--text-light);"><strong>你的4个维度详细偏好:</strong></p>
|
||||
<p style="font-size:13px;color:var(--text-light);">${ei.codeA}/${ei.codeB}(${ei.scoreA}/${ei.scoreB}) → <strong>${ei.result}</strong> · ${sn.codeA}/${sn.codeB}(${sn.scoreA}/${sn.scoreB}) → <strong>${sn.result}</strong> · ${tf.codeA}/${tf.codeB}(${tf.scoreA}/${tf.scoreB}) → <strong>${tf.result}</strong> · ${jp.codeA}/${jp.codeB}(${jp.scoreA}/${jp.scoreB}) → <strong>${jp.result}</strong></p>
|
||||
<p style="font-size:13px;color:var(--text-light);margin-top:4px;">${dimResults.filter(d=>d.certainty).map(d=>'<strong>'+d.name.split(' ')[0]+'</strong>处于边界值').join(';') || '所有维度倾向均较明确'}</p>
|
||||
`;
|
||||
}
|
||||
|
||||
// ========== localStorage Persistence ==========
|
||||
const STORAGE_KEY = 'mbti_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 {
|
||||
const typeDescriptions = {
|
||||
'INTJ': '建筑师','INTP': '逻辑学家','ENTJ': '指挥官','ENTP': '辩论家',
|
||||
'INFJ': '提倡者','INFP': '调停者','ENFJ': '主人公','ENFP': '竞选者',
|
||||
'ISTJ': '物流师','ISFJ': '守卫者','ESTJ': '总经理','ESFJ': '执政官',
|
||||
'ISTP': '鉴赏家','ISFP': '探险家','ESTP': '企业家','ESFP': '表演者'
|
||||
};
|
||||
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-type">${h.type}</span>
|
||||
<span class="hi-name">${typeDescriptions[h.type] || ''}</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
|
||||
document.getElementById('resultType').textContent = h.type;
|
||||
document.getElementById('resultTimestamp').textContent = '⏰ 测试时间:' + h.timestampFormatted;
|
||||
displayMBTIResult(h.type, h.details);
|
||||
|
||||
document.getElementById('results').classList.add('show');
|
||||
document.getElementById('results').scrollIntoView({ behavior: 'smooth', block: 'start' });
|
||||
}
|
||||
|
||||
function resetTest() {
|
||||
if (!confirm('确定要重新测试吗?当前结果已自动保存到历史记录中,不会丢失。')) return;
|
||||
// Reset all answers
|
||||
dimensions.forEach(d => { for (let i = 0; i < 7; i++) answers[d.id + i] = null; });
|
||||
// Hide results
|
||||
document.getElementById('results').classList.remove('show');
|
||||
document.getElementById('resultType').textContent = '';
|
||||
document.getElementById('resultTimestamp').textContent = '';
|
||||
document.getElementById('dimDetails').innerHTML = '';
|
||||
document.getElementById('interpretation').innerHTML = '';
|
||||
// Re-render questions
|
||||
renderAll();
|
||||
// Scroll to top
|
||||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||||
}
|
||||
|
||||
// ========== 提交到服务器 ==========
|
||||
async function submitToServer() {
|
||||
const type = document.getElementById('resultType').textContent;
|
||||
if (!type) { alert('请先完成测试再提交'); return; }
|
||||
|
||||
const dimResults = dimensions.map(d => {
|
||||
let countA = 0;
|
||||
for (let i = 0; i < 7; i++) if (answers[d.id + i] === 'A') countA++;
|
||||
return {
|
||||
id: d.id, name: d.name, codeA: d.codeA, codeB: d.codeB,
|
||||
result: countA >= 4 ? d.codeA : d.codeB,
|
||||
scoreA: countA, scoreB: 7 - countA
|
||||
};
|
||||
});
|
||||
|
||||
try {
|
||||
const resp = await fetch('/api/standalone/submit', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
test_name: 'mbti',
|
||||
user_name: '天恒',
|
||||
result: type,
|
||||
full_data: { type, dimResults }
|
||||
})
|
||||
});
|
||||
const result = await resp.json();
|
||||
if (result.success) {
|
||||
alert('✅ MBTI测试结果已成功提交到服务器!\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 = `MBTI测试结果_${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 = `MBTI测试全部历史_${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>
|
||||
Reference in New Issue
Block a user