玄学放置 · 登录页面与前端认证体系
起因
上一篇刚搭完后端骨架——Fastify + PostgreSQL + JWT 鉴权,6 张表和离线进度结算都就位了。但后端跑起来之后,前端还是直接渲染 HomeView,所有状态在 Pinia store 里本地跑,刷新就丢档。后端的 /api/auth/register 和 /api/auth/login 没有调用方,等于建了桥没有路。
放置游戏的核心循环是"挂机后回来收菜",刷新丢档体验不成立。而登录页面是把前端从纯本地 SPA 过渡到"后端存档、前端展示"的第一步——玩家身份需要先确立,才有后续的存档、离线结算、每日事件。
先解决架构问题,再写页面
项目之前没有 Vue Router。App.vue 直接挂 <HomeView />,没有路由概念。加登录页不是简单地"多写一个组件",而是需要一套完整的认证架构:
- 路由守卫:未登录自动跳转
/login,已登录跳转/时回 Home - 认证 store:管理 JWT、玩家信息、游客模式,持久化到 localStorage
- 定时器归属:原来 App.vue 里跑 200ms tick,登录页不需要跑,得把定时器移到 HomeView 内部
这三个问题里,定时器归属看似最小,其实最容易忽略。如果登录页激活时 tick 仍然在跑,subjectsStore 里没有数据就会报空指针。把它移到 HomeView 的 onMounted/onBeforeUnmount 里,让定时器的生命周期和 Home 路由绑定,是更自然的归属。
游客登录:零门槛进入
放置游戏的留存很脆弱——如果玩家第一次打开就要填用户名和密码,很多人会直接关掉。游客登录是降低这个门槛的标准做法:点一下就进,数据照常存,后续可以在设置里绑定邮箱升级为正式账号。
实现上,游客登录其实是自动注册:前端生成 guest_ + 6 位随机字符作为用户名,12 位随机字符串作为密码,然后调后端的 /api/auth/register 接口。后端不区分游客和正式用户——区别只在前端 auth store 的 isGuest 标记,用于 UI 展示和后续的升级引导。
async function guestLogin(): Promise<{ ok: boolean; error?: string }> {
const guestName = `guest_${randomChars(6)}`
const guestPwd = randomChars(12)
const res = await fetch('/api/auth/register', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username: guestName, password: guestPwd })
})
const data = await res.json()
if (!res.ok) return { ok: false, error: data.error }
setSession(data, true) // isGuest = true
return { ok: true }
}密码不需要用户记住——它和 token 一起存在 localStorage 里,下次打开浏览器自动恢复。游客模式下用户感知不到密码的存在,体验和无账号游戏一样流畅。
登录页的视觉:暗色霓虹 + 像素浮标
HomeView 的头部品牌区用了 🪐 + 霓虹青色发光的"学科放置"标题。登录页延续了这套视觉语言,但做了更戏剧化的处理——因为登录页是全屏居中布局,不像 HomeView 那样被侧栏和面板分割,可以放开手脚做氛围。
核心视觉元素:
- 全屏暗色背景,复用
--bg-base+ 径向光晕(和 global.scss 的 body 背景一脉相承) - 居中卡片用
.panel样式——--bg-panel+--border-soft+--radius-md,加一层更重的 box-shadow 做聚焦感 - 品牌区和 HomeView 头部完全一致,保持跨页面的视觉连续
- 6 个 PixelIcon 做浮动装饰——math、physics、mysticism、biology、chemistry、history,各自的学科主题色做微弱发光,
float动画 6-8 秒周期上下缓动
浮动图标的位置分布是手工配的,不是随机——左右各 3 个,避免和中央卡片重叠。opacity 设 0.4,足够看见但不抢焦点。这种"角落里有东西在动"的氛围感,比纯静态背景更能留住第一眼注意力。
账号登录/注册:同一表单的两种状态
登录和注册的表单差异很小——注册只多一个"确认密码"字段。与其做两个独立组件,不如用一个表单,通过 isRegister 标记动态切换:
- 按钮文字在"登录"和"注册"之间切换
- 确认密码字段
v-if="isRegister"条件渲染 - 校验规则里,确认密码的
required和自定义 validator 都绑定isRegister——非注册模式下这组规则直接跳过
const rules = computed<FormRules>(() => ({
username: [
{ required: true, message: '请输入用户名', trigger: 'blur' },
{ min: 2, max: 50, message: '2-50 个字符', trigger: 'blur' }
],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' },
{ min: 6, max: 100, message: '至少 6 位', trigger: 'blur' }
],
confirmPassword: [
{ required: isRegister.value, message: '请确认密码', trigger: 'blur' },
{ validator: validateConfirmPassword, trigger: 'blur' }
]
}))rules 用 computed 而不是 ref,是因为 Element Plus 的 el-form 需要响应式的规则对象——当 isRegister 切换时,规则会自动重新计算,确认密码字段的校验立即生效或失效。
el-input 的暗色适配
项目之前只覆盖了 el-input-number 的暗色样式。登录表单的 el-input 需要同样的处理——Element Plus 默认的白色背景在深色面板上非常刺眼。
补充的覆盖和 el-input-number 完全同构:background-color: var(--bg-base),box-shadow 用 --border-soft 做边框,hover 加强到 --border-strong,focus 用 --accent-cyan 做高亮。另外补了 .el-form-item__label 的颜色——默认的 #606266 在暗色背景下对比度不够,改为 --text-secondary。
这些覆盖写在 element-overrides.scss 里而不是组件的 scoped style 里,因为 el-input 的内部 DOM 是通过 CSS 类名控制的,scoped style 穿不透。
踩到的坑
vue-router 的安装时机。项目之前没有 router,直接 pnpm add vue-router@4 装好后,main.ts 里要 app.use(router) 注册。但注册顺序有讲究——router 必须在 app.mount('#app') 之前调用,而且 pinia 需要在 router 之前注册,因为路由守卫里可能用到 store。当前顺序是 pinia → router → ElementPlus → mount,跑通了但花了些时间排查顺序问题。
confirmPassword 校验器的 trigger。如果 confirmPassword 的 validator 只设了 trigger: 'blur',而用户在注册模式下填完密码直接切换回登录模式,el-form 内部的校验状态不会自动清除。这种情况暂未处理——用户切换模式时表单会整体重置,问题不大,但值得留意。
登录页的 el-input prefix-icon。LoginForm 里给 el-input 加了 :prefix-icon="User" 和 :prefix-icon="Lock",这些是从 @element-plus/icons-vue 导入的。图标颜色在暗色背景下偏暗,需要在覆盖里加 .el-input__prefix .el-icon { color: var(--text-muted) } 才能和整体风格协调。
收获
这次改动让项目从"纯前端 SPA"正式变成了"有认证流程的应用"。虽然后端还没跑起来(需要安装 PostgreSQL),但整个前后端衔接的链路已经设计好了——前端发请求,后端验 JWT,路由守卫控制页面访问。
游客登录是最值得做的设计决策。它让"第一次打开游戏"这个关键瞬间的摩擦降到最低——点击、等待、进入,三步到位。至于游客和正式用户的区别,那是后续设置面板里的事,不在登录页制造焦虑。
从架构上看,这次改动建立了几个会长期存在的模式:路由守卫 + auth store + localStorage 持久化,这三者的组合是大多数需要"登录态"的前端应用的标配。后续如果要加 OAuth(微信/Google 登录),只需要在 auth store 里多一个 oauthLogin action,路由守卫和持久化逻辑完全不用动。
下一篇大概率会回到后端——安装 PostgreSQL、建库跑迁移、让后端真正跑起来,然后把前端从 mock 数据源切换到后端 API。那才是前后端真正接通的时刻。