制作一个 MBTI 人格测试网页项目
我一直对心理学很感兴趣,尤其是 MBTI 人格测试这个话题,每次和朋友讨论起“你是 INFJ 吗?”、“他八成是个 ESTP”之类的桥段,总会引发一连串的共鸣和好奇。所以在一次创作中,我决定把这个兴趣落地为一个真正可用、体验良好且外观精美的 MBTI 人格测试网站,最终上线。
这篇文章就是我对整个项目开发过程的完整回顾。从页面交互设计、代码组织结构,到动画细节、数据模型与心理学逻辑,我都会尽量详实地记录下来,哪怕是一条看似简单的样式,我都尝试讲清它背后的思考。希望这不是一篇教程式的说明书,而是一次充满温度的创作记录。
项目的最初构思:如何让“测试”变得温柔些?
最开始我并不想直接做一个“冷冰冰”的测试表单,而是希望能营造一种心理游戏式的体验。我的目标用户也不是心理学专业人士,而是像我一样对人格测试感兴趣的普通人。我希望他们在进入这个网站时,就能被一种温柔、治愈的气质包围,而不是被问卷压得喘不过气。
所以我第一步先定了整体基调:视觉要清新,配色温柔,交互舒适流畅。基于这些方向,我选用了 Vue 3 + Vite 作为技术栈,UI 组件选择了 Element Plus,动画则配合 anime.js 和 echarts 来完成必要的图形反馈。整站只有 3 个核心页面:首页、测试页和结果页,我把所有的逻辑都控制在这三页之中,让整个项目保持轻量,但功能齐全。
整体结构设计:简洁三页式设计背后的权衡
首页设计思路
我设计首页的初衷是“不要急着做题,先感受氛围”。所以它的主视觉部分是一个渐变背景,搭配粉白色调,并设计了浮动樱花动画。在视觉上制造一种淡雅的安静感。这种感觉有点像走进了一家日式心理咖啡馆。
我用了一个小标题引导:“探索属于你的人格气质”,再用一个按钮承接“开始测试”。页面中间我放了四个图标:MBTI介绍、性格类型、理论背景、关于本站,这些其实都是静态卡片,但通过 hover 动效做出柔和的响应,不至于显得生硬。
首页代码使用 flex
和 vh
单位确保自适应布局,渐变背景使用了线性渐变和动画:
<template><div class="home-container"><div class="slogan"><h1>探索属于你的人格气质</h1><el-button type="primary" @click="goToTest">开始测试</el-button></div><div class="features"><el-card v-for="feature in featureList" :key="feature.title"><h3>{{ feature.title }}</h3><p>{{ feature.description }}</p></el-card></div></div>
</template>
.home-container {background: linear-gradient(135deg, #fdeff9, #ecf0ff);height: 100vh;display: flex;flex-direction: column;justify-content: center;align-items: center;.slogan {text-align: center;h1 {font-size: 2.5rem;color: #444;}}
}
虽然只是几个容器,但我在 hover 和 focus 状态下补上了圆角放大、微妙阴影以及 transition
效果,从而让卡片在视觉上“呼吸”起来。
流程图:从首页到测试完成的流程思维
为了理清用户在站内的行为流转,我一开始就画了以下这张结构图,确认每个页面之间的跳转路径与信息传递逻辑。这个流程图是我后续开发过程中的导航灯塔。
这个图虽然简单,但极大帮助我在后续组件传参、路由跳转、状态管理中避免走弯路,Pinia 中的状态设计也直接围绕这个流程展开。
性格测试页的构建过程:状态管理与交互逻辑
测试页是整个项目最复杂的部分,既要动态显示题目,又要处理用户选项,还要支持中途返回重新作答。我一开始用了一个 questions
数组存储所有题目和选项,每题都是一对相对倾向,比如 “你更喜欢专注思考还是现实操作?”,用户通过左/右滑块或按钮选择更接近的选项。
我使用 Pinia 来维护全局状态。状态包括:
export const useMbtiStore = defineStore('mbti', {state: () => ({currentIndex: 0,answers: [] as string[],result: '',}),actions: {recordAnswer(answer: string) {this.answers.push(answer);this.currentIndex++;},reset() {this.answers = [];this.currentIndex = 0;}}
});
组件内通过 computed
获取当前题目,通过 watch
监听当前题目索引变化,控制进度条与交互反馈,逻辑清晰直观。点击选项按钮后立即保存答案并切换下一题,最后一题提交后跳转到结果页。
我没有使用表单提交或统一一次性上传,而是用每次作答立即记录的方式,在用户切换页面时也能保持状态。
性格计算算法设计:四维度拆解判断
MBTI 分为四个维度(E/I、S/N、T/F、J/P),每个维度有若干个问题代表某一方向。我在设计题目时,给每题加了一个 dimension
字段,这样就能在提交后统计各维度中用户选择的倾向。
最终人格类型是通过简单对比每个维度中得票高的那一方得出。例如:
function calculateMBTI(answers: string[]): string {const dimensionScore = {E: 0, I: 0,S: 0, N: 0,T: 0, F: 0,J: 0, P: 0,};answers.forEach(ans => {dimensionScore[ans]++;});return [dimensionScore.E > dimensionScore.I ? 'E' : 'I',dimensionScore.S > dimensionScore.N ? 'S' : 'N',dimensionScore.T > dimensionScore.F ? 'T' : 'F',dimensionScore.J > dimensionScore.P ? 'J' : 'P',].join('');
}
这个算法虽然简单,但非常直观易于调试,逻辑一目了然。
结果页的视觉设计与图表生成
结果页是这个项目中最需要“氛围感”的部分。我不想只显示“你是 INFJ”,那样未免太冰冷,所以我展示了:
- 性格类型的详细介绍
- 优势与劣势
- 推荐职业
- 同类型人物
- 四维雷达图展示倾向分布
视觉上,我用 Echarts 生成雷达图,初次进入时附带渐入动画,四个维度分布一目了然。下方用卡片方式展示详细信息,配合合适的行间距和配图,使整个页面富有层次感与阅读节奏。
雷达图数据准备如下:
const chartData = {indicator: [{ name: '外向/内向', max: 10 },{ name: '感觉/直觉', max: 10 },{ name: '思维/情感', max: 10 },{ name: '判断/知觉', max: 10 },],value: [7, 3, 9, 5]
};
Echarts 配置中我设置了 areaStyle
与 shadowColor
,营造一种柔软包裹的视觉质感,曲线也设置为平滑贝塞尔曲线,避免尖锐突兀。
动效设计:从细节让页面“呼吸”起来
虽然是一个功能型网站,但我一直觉得,动效对于用户体验来说并不是“锦上添花”,而是“必需品”。一个页面哪怕内容再有价值,如果交互生硬,切换突兀,都会让人觉得有些“卡”。所以在这个 MBTI 网站中,我对动画做了很多打磨。
Vue 的过渡系统给了我很大的帮助。在测试页切换题目的时候,我加了一个简单的左右滑动动效,让问题像“翻书”一样地过渡:
<transition name="slide-fade" mode="out-in"><QuestionCard :question="currentQuestion" />
</transition>
.slide-fade-enter-active,
.slide-fade-leave-active {transition: all 0.4s ease;
}
.slide-fade-enter {transform: translateX(30px);opacity: 0;
}
.slide-fade-leave-to {transform: translateX(-30px);opacity: 0;
}
同时,在每次完成作答进入下一题的瞬间,我还使用 anime.js
给按钮加入了缩放反馈,用 scale(1.1) -> scale(1)
的方式做出一种“确认”感,增强用户点击后的满足感。这些动画看似简单,但让页面在静与动之间取得了很好的平衡。
响应式设计:从手机到桌面都要温柔呈现
一开始我只在桌面上做开发,等到测试的时候我才意识到这个项目非常适合移动端使用。大多数人会在微信或手机浏览器中打开,所以我花了不少时间做了响应式适配。
我没有使用 Tailwind 或 bootstrap 之类的 UI 框架,而是选择纯 CSS + SCSS 实现响应式布局。用 @media
媒体查询,配合 flex
和百分比宽度,来实现不同屏幕下的自动适配。
在测试页中,每一个题目组件都是 100% 宽度的卡片结构,在桌面上会居中展示、留出左右白边;而在手机上则会贴边,全屏渲染,提升沉浸感:
.question-card {width: 100%;max-width: 600px;margin: auto;padding: 2rem;background: white;border-radius: 12px;box-shadow: 0 8px 24px rgba(0, 0, 0, 0.05);@media screen and (max-width: 768px) {padding: 1.2rem;border-radius: 0;box-shadow: none;}
}
我也写了 rem
单位的 font-size
适配脚本,通过设置 html { font-size: 62.5%; }
再统一使用 rem
,使得字体在不同分辨率下大小合适。
全局样式与主题控制:用 SCSS 和变量统一规范
为了让样式更加可控和统一,我在项目中引入了 SCSS,并配置了一套全局变量系统。比如颜色、圆角、阴影、字号等等,都集中管理,这不仅方便日后维护,也有助于组件之间保持一致的风格。
// styles/variables.scss
$primary-color: #7c83fd;
$secondary-color: #f8f9fc;
$text-color: #333;
$border-radius: 12px;
$shadow: 0 4px 16px rgba(0, 0, 0, 0.06);
在每个组件中,我都用变量替代硬编码颜色,比如:
.card {background-color: $secondary-color;border-radius: $border-radius;box-shadow: $shadow;
}
这样做带来了意外的好处:后期我加入深色模式切换时,只需要重新定义一套变量,而不需要重写所有组件的样式逻辑。
组件设计:结构清晰,职责明确
虽然整个站点页面不多,但我还是坚持把功能拆成了独立组件。比如:
QuestionCard.vue
:单个题目卡片ProgressBar.vue
:顶部进度条ResultRadar.vue
:Echarts 生成雷达图ResultDetail.vue
:性格说明文字卡片
每个组件都有单独的 props 定义和局部样式,保持了良好的职责分离。Vue3 的 <script setup>
写法也让代码结构非常清爽,配合 TS 类型推导,即使组件间传参也不容易出错。
状态管理:Pinia 的使用体验
我之前在一些项目中使用 Vuex,后来用了 Pinia 之后就彻底“转粉”了。它的 API 更简洁,组合式逻辑更清晰,类型推导也优秀。
我创建了一个 mbti.ts
的 store 文件,负责管理当前题目索引、用户答案、最终结果。组件中通过 useMbtiStore()
获取状态并调用更新方法。
// store/mbti.ts
export const useMbtiStore = defineStore('mbti', {state: () => ({currentIndex: 0,answers: [] as string[],result: ''}),getters: {currentQuestion(state) {return questions[state.currentIndex];},isFinished(state) {return state.answers.length >= questions.length;}},actions: {next(answer: string) {this.answers.push(answer);this.currentIndex++;},calculate() {this.result = calculateMBTI(this.answers);},reset() {this.answers = [];this.currentIndex = 0;}}
});
结果页的跳转也是通过 store 控制逻辑判断:只有在作答完毕后才能跳转,避免跳过题目。
小插曲:从主题崩坏到渐变重构
有一次我尝试加入粉蓝渐变背景时,直接在 body
上用了 linear-gradient
。结果部分移动端浏览器因为 height: 100%
设置不正确导致渐变只渲染到可视区域的一半,下面是白屏。
我一开始以为是某个 CSS 被覆盖了,结果调了半天才发现是页面内容高度不够导致 body
没撑开。最后的解决办法是在 html, body
上强制设定 min-height: 100vh;
,并用 flex: 1
保证容器占满剩余空间,才成功解决。
这是前端开发中常见却很隐蔽的问题,写起来一句话,调试起来几个小时……
流程图补充:状态变化之间的逻辑链路
在结果页完成计算之前,我还加了一个“处理中…”的等待动画页,用来模拟分析过程。这部分虽然只是一个过渡状态,但也是用户体验的重要节点。
以下是状态变化的更详细逻辑图:
这个状态流很适合用状态机来实现,不过项目规模不大,我最终用 Pinia 状态 + Vue 路由跳转实现了全部逻辑。
题库结构设计:保持灵活与可扩展性
我一开始是把题目硬编码在组件内部的,但很快发现这个做法既不利于维护,也不方便扩展或更换题库。于是我将所有题目独立抽离到一个专门的 questions.ts
模块中,用标准化的数据结构来组织。
每道题都由四个字段组成:题干、两个选项、对应的维度标记。
// data/questions.ts
export const questions = [{question: '当你参加一个社交聚会时,你更倾向于?',optionA: { text: '与很多人交谈', trait: 'E' },optionB: { text: '只与熟人聊天', trait: 'I' }},{question: '你更相信哪种判断方式?',optionA: { text: '逻辑与客观分析', trait: 'T' },optionB: { text: '情感与个人价值', trait: 'F' }},// ...共40题
];
这种结构的好处是逻辑简单,扩展方便。后续如果要做不同语言的题库,也只需要提供多语言版本的数据文件,而无需修改组件逻辑。
在组件中通过当前索引动态渲染题目文本与选项,同时记录用户选择的 trait
字母,最终累积成四个维度的对比统计:
const traitCount = {E: 0, I: 0,S: 0, N: 0,T: 0, F: 0,J: 0, P: 0
};answers.forEach(trait => {traitCount[trait]++;
});
最后根据每组维度中数量较多的那个,组合出最终 MBTI 类型。
Vue Router:页面跳转的“仪表盘”
MBTI 测试共包含 4 个页面:首页 / 测试页 / 加载页 / 结果页
。我使用 Vue Router 来组织这些页面的导航结构。
这不是一个 SPA 式的滚动单页,而是标准的路由切换式导航。页面结构在 router/index.ts
中这样定义:
const routes = [{ path: '/', component: Home },{ path: '/quiz', component: Quiz },{ path: '/loading', component: Loading },{ path: '/result', component: Result }
];
这种方式不仅清晰分层,还能更好地控制每页的 meta 信息与生命周期行为。例如我在结果页加入了如下钩子:
onMounted(() => {if (!store.result) {router.push('/');}
});
这个判断用来防止用户在未答题的情况下直接访问结果页,确保路径跳转的合理性和数据一致性。
另外,我为每个页面都设定了 <meta name="description">
和 document.title
,以便于 SEO 和分享到微信、微博时有更好的预览效果。
数据缓存:为刷新与返回设计容错机制
在最初的版本中,如果用户在测试过程中刷新了页面,之前的答题记录就会完全丢失。我发现这样体验非常糟糕。于是我加入了一个简单的 localStorage
缓存机制。
每次答题时,我都会将当前进度与答案数组同步存入本地:
watch(() => store.answers,() => {localStorage.setItem('mbti-progress', JSON.stringify({answers: store.answers,index: store.currentIndex}));},{ deep: true }
);
当用户再次访问测试页时,我会检查是否存在缓存,并提示是否恢复:
const saved = localStorage.getItem('mbti-progress');
if (saved) {const { answers, index } = JSON.parse(saved);if (answers.length < questions.length) {store.answers = answers;store.currentIndex = index;}
}
虽然只是一个简单的缓存方案,但对于移动端用户来说,它极大提升了容错性与用户友好度。
图表设计:ECharts 自定义雷达图美化
MBTI 的四个维度最适合的可视化方式就是雷达图。我用 ECharts 实现了一个动态雷达图,展示用户的四个维度得分比例。
const option = {radar: {indicator: [{ name: '外向/内向', max: 20 },{ name: '感知/直觉', max: 20 },{ name: '思维/情感', max: 20 },{ name: '判断/感知', max: 20 }]},series: [{type: 'radar',data: [{value: [store.ei, store.sn, store.tf, store.jp],name: '你的 MBTI'}]}]
};
我还特别在颜色、字体、渐变线条上下了功夫,让图表更贴合整站的柔和 UI 风格。
图表在移动端上默认宽度为 100%,并通过 resize
事件自适应屏幕宽度。为了让图表初次加载不闪烁,我加入了 nextTick
配合 ref
的加载方式:
onMounted(() => {nextTick(() => {myChart.value = echarts.init(chartRef.value);myChart.value.setOption(option);});
});
项目结构优化:从零碎到清晰
刚开始项目文件很乱,组件、页面、样式都混在一起。开发到中期,我痛下决心做了一次目录结构的优化,现在的结构如下:
src/
├── assets/ # 图片资源
├── components/ # 通用组件
├── data/ # 题库数据
├── pages/ # 页面组件
├── router/ # 路由配置
├── store/ # Pinia 状态管理
├── styles/ # 全局 SCSS
└── utils/ # 工具函数
这种结构对于协作开发、维护迭代都非常友好。每个模块的职责明确,阅读成本低。
页面效果一览(结构草图)
我还为网站整体结构绘制了一份草图图示,用于说明页面之间的层次关系与跳转路径:
这份结构图在我调试路由与状态同步时发挥了重要作用,帮助我快速梳理页面流程与跳转逻辑。
工具函数封装:小功能不小作用
开发过程中,我逐渐发现有些逻辑重复出现在多个页面中,比如获取当前 MBTI 字符串、清除缓存、分享链接构建等。如果直接写在页面逻辑中,会让代码显得臃肿且难维护。
于是我将这部分逻辑封装进 utils
文件夹,形成了一些小而美的工具函数:
// utils/mbti.tsexport function getMbtiType(traitCount: Record<string, number>): string {return [traitCount['E'] >= traitCount['I'] ? 'E' : 'I',traitCount['S'] >= traitCount['N'] ? 'S' : 'N',traitCount['T'] >= traitCount['F'] ? 'T' : 'F',traitCount['J'] >= traitCount['P'] ? 'J' : 'P'].join('');
}export function clearLocalCache() {localStorage.removeItem('mbti-progress');localStorage.removeItem('mbti-result');
}
这些函数配合 Pinia 中的 store 使用,可以极大地减少模板内逻辑的复杂性。比如:
const result = getMbtiType(store.traitCount);
功能虽然不复杂,但在项目中实实在在节省了大量重复劳动。
动画与交互细节:打磨才是关键
一个 UI 看上去漂亮与否,除了布局和颜色,最重要的就是「动效细节」。
我特别为测试页的题目切换加上了渐隐渐现的过渡动画,使用的是 Vue 内建的 <transition>
标签:
<transition name="fade" mode="out-in"><div :key="currentQuestionIndex"><QuestionItem :data="currentQuestion" /></div>
</transition>
对应的 CSS:
.fade-enter-active, .fade-leave-active {transition: opacity 0.5s ease;
}
.fade-enter-from, .fade-leave-to {opacity: 0;
}
这种细腻的过渡不仅提升了体验,也降低了用户在切换题目时的认知压力,尤其在移动端显得尤为重要。
此外,我还在按钮上加入了按下回弹的 scale
效果,让用户每次点击都感到「有反馈」:
button:active {transform: scale(0.95);
}
结果页内容:文字 + 图表 + 个性化推荐
结果页并不是简单的显示“你的类型是 INFP”,我希望它能承载更多内容,让用户感到这个测试有深度、有用、有趣。
所以我为每种 MBTI 类型都准备了一段简洁的性格介绍、适合的职业方向,以及代表性的动漫角色参考:
// data/traits.ts
export const mbtiProfiles = {INFP: {name: "调停者",description: "富有理想,追求内在价值,喜欢独处与自我探索。",career: ["作家", "心理咨询师", "创意设计"],characters: ["长门有希", "阿良良木历"]},ESTJ: {name: "监护人",description: "务实可靠,擅长组织事务,是典型的执行者。",career: ["项目经理", "公务员", "法务人员"],characters: ["伊尔迷", "卡卡西"]}
}
渲染时我用图文结合方式展示,让页面丰富但不杂乱:
<h2>你的性格类型:INFP「调停者」</h2>
<p>你注重内在体验,常常有丰富的情感和想象力...</p><h3>适合职业:</h3>
<ul><li>心理咨询师</li><li>文案编辑</li>
</ul><h3>代表角色:</h3>
<div class="characters"><img src="/imgs/nagato.jpg" alt="长门有希" /><img src="/imgs/aragi.jpg" alt="阿良良木历" />
</div>
搭配渐变背景与柔光卡片 UI,用户在浏览结果时会有一种轻松愉悦的感觉,而不是冰冷的文字。
社交分享卡片生成:前端动手做一切
为了让用户能分享到朋友圈、微博等,我开发了一个小模块用于生成分享卡片,效果类似于知乎分享图:
- 卡片上包含昵称、测试时间、MBTI 类型图标、二维码等;
- 使用 html2canvas 将结果页部分内容截屏为图片;
- 允许用户保存图片或长按识别。
这部分实现我用的是一个隐藏区域 + html2canvas
:
import html2canvas from 'html2canvas';async function generateShareImage() {const element = document.getElementById('share-area');const canvas = await html2canvas(element);const url = canvas.toDataURL('image/png');downloadImage(url, 'mbti-result.png');
}
配套 UI:
<div id="share-area" class="share-card"><h2>{{ userMbti }}</h2><p>测评时间:{{ time }}</p><img :src="qrCode" />
</div>
虽然这不是一个必要功能,但它极大提升了网站的趣味性和传播力。每次我测试后生成图发到朋友圈,总会有人点进来看并留言「我也是 INFP」什么的。
页面风格与色彩搭配:现代感+治愈系
配色方面我没有走极简黑白风,而是采用了「淡紫 + 柔绿 + 雾蓝」的渐变色调,营造一种温暖、安静又带点科幻感的氛围。
主色调配比如下:
- 主背景色:
#f5f6fa
- 按钮色:
#7c8bd0
- 高亮文本:
#4c9aff
- 错误提示:
#ff6b81
全站使用了 SCSS 全局变量:
$color-primary: #7c8bd0;
$color-secondary: #a4d4ae;
$color-bg: #f5f6fa;
$color-danger: #ff6b81;
再配合玻璃拟态风格(backdrop-filter
+ box-shadow
),页面整体感觉不再是单薄的静态网页,而像一个轻巧但功能完整的「App」。
项目打包与部署:静态托管一点也不麻烦
整个项目开发完成后,我用 vite build
构建为静态文件,并选择部署到 GitHub Pages:
vite build
cp -r dist/* ../mbti-pages/
git push
GitHub Pages 配置了自定义域名 https://mbti.onefan.top
,响应速度尚可,全球可访问,不存在国内用户打不开的问题。
同时我还写了一个 deploy.sh
脚本,每次部署一键完成:
#!/bin/bash
pnpm run build
cp -r dist/* ../mbti-pages/
cd ../mbti-pages
git add .
git commit -m "deploy"
git push
SEO 与社交媒体优化:细节决定传播力
虽然是一个轻量的 MBTI 测试网站,但我在上线前还是做了一些 SEO 和社交分享优化。主要包括:
- 为每个页面设置
<title>
和<meta>
标签; - 首页和结果页增加
og:
和twitter:
协议的分享信息; - 针对微信等无法读取 meta 的平台,增加带有摘要信息的分享图。
示例:
<!-- index.html -->
<head><title>MBTI人格测试 | 简洁可爱的心理测试站</title><meta name="description" content="基于MBTI理论的免费心理测试网站,快速了解你的性格类型。"/><meta property="og:title" content="MBTI人格测试 - 性格探索从这里开始"/><meta property="og:description" content="免费测试你的 MBTI 类型,看看你属于哪一类人格!"/><meta property="og:image" content="/imgs/share.png"/>
</head>
虽然这些元信息不会直接让用户看到,但在百度、Google、微信浏览器、QQ 浏览器的分享卡片中,会影响点击率和传播力。属于那种「不做没人说,做了效果立现」的优化。
适配移动端体验:小屏幕不妥协
从一开始我就假设用户主要来自手机,所以整个 UI 从字体到按钮大小、内容布局,都优先为移动端设计。
我使用了 rem
单位和媒体查询辅助响应式适配,配合 viewport
控制:
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0"/>
CSS 中部分关键样式:
html {font-size: 16px;@media screen and (max-width: 768px) {font-size: 14px;}
}
此外,我为每个模块设置了最大宽度和边距,让其在大屏浏览器中也不会左右拉伸过长,确保阅读体验:
.container {max-width: 768px;margin: 0 auto;padding: 1.5rem;
}
有意思的是,微信、QQ 的内置浏览器对 vh
单位存在 bug(尤其 iOS),我还特地用 JS 修正了一下页面高度:
function fixViewportHeight() {const vh = window.innerHeight * 0.01;document.documentElement.style.setProperty('--vh', `${vh}px`);
}
window.addEventListener('resize', fixViewportHeight);
fixViewportHeight();
这使得我们在 CSS 中可以使用 height: calc(var(--vh, 1vh) * 100)
,避免了手机输入框弹出时页面错乱的问题。
流程设计回顾:从进入到完成的完整旅程
这个小项目虽然页面不多,但从用户第一次访问,到完成测试、查看结果并分享,我还是做了一个完整流程图来帮助梳理整个用户体验路径:
每一步都有对应的页面、数据记录方式和用户交互。流程清晰才能避免用户迷路或中途放弃。
本地缓存与容错:不怕用户中途离开
有些用户可能会在做一半测试的时候关掉页面或误触刷新,我考虑到了这个情况,使用 localStorage
存储答题进度和当前题目序号。
watch(() => store.currentIndex, (val) => {localStorage.setItem('mbti-progress', JSON.stringify({current: val,traitCount: store.traitCount}));
}, { deep: true });
当用户再次访问时,程序会检测缓存并弹出提示:
const cache = localStorage.getItem('mbti-progress');
if (cache) {const confirm = window.confirm('检测到上次未完成的测试,是否继续?');if (confirm) {// 恢复测试进度} else {clearLocalCache();}
}
这样就算中途退出也不会浪费时间。也增强了用户粘性。
自动打乱题目顺序:防止记忆刷答案
为了提升测试的客观性,我没有固定题目顺序,而是在用户进入测试页时自动打乱:
function shuffle<T>(arr: T[]): T[] {return arr.map(item => ({ item, sort: Math.random() })).sort((a, b) => a.sort - b.sort).map(obj => obj.item);
}
每次加载测试页时我调用 shuffle(questions)
,确保每个人顺序不同,有效防止了“直接背答案”行为。
总结与收获
这次做 MBTI 测试网站的经历不算漫长,但确实让我把 Vue3、TypeScript、Pinia、SCSS、前端动画、静态图生成等多个知识点都实践了一遍,也在 UI/UX 的打磨上有了更高要求。
从一开始的“能跑起来”,到后来“好用”、“好看”、“能传播”,一步一步优化下来,整个过程既有成就感,也极具挑战。
最重要的是,每次朋友看到分享图都说“这个测试有点意思”,那种被认可的感觉,值了。
参考地址:https://gitee.com/Twilight-Fanyi/mbti-personality