1. 从标题说开:这个登录页到底要解决什么问题
做 uni-app 微信小程序的这几年,我发现一个挺有意思的现象:技术群里问得最多的往往不是复杂业务逻辑,而是"登录页怎么做才好看"。这看起来是个小问题,但真动手写过的都知道,登录页是整个小程序里性价比最低又最难讨好的一个页面——它逻辑不复杂,却要在几十行代码里同时扛住视觉、交互、性能、适配四件事。这一篇是"好看的 ui 登录页面"系列的第四篇,前三篇把基础骨架、渐变背景、表单结构都过了一遍,这篇我打算把重点放在视觉细节的参数化落地和登录链路的完整闭环上,也就是把"看起来好看"变成"每一处都能说出为什么这么写"。
先明确这篇的读者定位。如果你已经在用 uni-app 开发微信小程序,写过至少一个能跑起来的项目,那这篇基本可以当作模板直接抄;如果你刚接触 uni-app,只看过官方文档里的 hello world,那也能看懂,只不过我建议你先搭一个空白页面边看边敲,纯看文字很容易漏掉细节。我自己的习惯是,任何一篇讲 UI 的文章,如果不配可运行的代码,基本等于没说,所以下面每一段关键的视觉参数和逻辑,我都会给出可以直接粘贴进.vue文件的代码。
1.1 登录页在整个小程序里的真实分量
很多人把登录页当成"入口那一关",过了就丢到脑后,这个认知其实挺吃亏的。登录页是小程序里用户停留时间最短、但触发网络请求最密集的页面之一:一次手机号输入、一次验证码发送、一次协议勾选、一次提交登录,每个动作背后都是状态变化。如果这个页面的状态管理写得乱,后面所有需要登录态的页面都会跟着遭殃——token 拿不到、用户信息是空的、退出登录后缓存没清干净,这些都是从登录页埋下的雷。
更现实一点说,登录页还是审核和体验的第一印象。微信小程序审核对登录流程的合规性有一定关注度,比如是否强制授权、是否默认勾选协议、是否有明确的用户知情提示。视觉上做得干净利落,体验上少一次多余的点击,用户流失率就能降下来。我做过一个对比:同一个项目,把登录页从"手机号 + 密码 + 图形码"三件套换成"手机号 + 短信验证码",注册转化在那次改动后提升了不少,原因很简单,用户忘密码的成本太高了。
所以这篇的定位不是"教你画一个漂亮的界面",而是把登录页当作一个小型状态机来设计:视觉是外壳,状态流转是内核,两者要一起考虑。标题里说的"好看",我理解成三层意思——配色舒服、层级清晰、动效不跳戏,缺一个都会显得廉价。
1.2 技术栈与版本前提
先把环境说清楚,避免你照着抄却跑不起来。这套页面的基础组合是uni-app + Vue3 组合式 API + script setup 语法糖,编译器版本建议在 3.0 以上,微信开发者工具的基础库选择 2.30 以上比较稳妥。为什么强调 Vue3,因为组合式 API 在登录页这种状态比较多但又不复杂的场景里优势很明显:逻辑可以按功能拆成几个独立的 ref 和 computed,不用像选项式那样在 data、methods、computed 之间来回跳。
HBuilderX 新建项目的时候直接选"uni-app 项目 → 默认模板 → Vue3",或者用 CLI 创建也可以。要注意一个坑:Vue3 项目的v-model在小程序端对自定义组件的支持和 H5 端不完全一致,如果后面你要抽一个自己的输入框组件,事件名得用update:modelValue,别用老写法。这个细节我在第二篇里提过一次,但每年都有新人踩,还是再强调一下。
另外,微信小程序的原生登录 API 是uni.login,它返回的code只能用来换 openid 和 session_key,不要拿它当业务 token 用。这句话我在后面第四章还会再拆开讲,因为这是新手最容易搞混的地方。
1.3 页面结构分层思路
我写登录页习惯先分层再写代码,一共分四层:最底下是背景层,负责渐变或者装饰性图形;往上是卡片层,承载所有表单元素;再往上是交互层,也就是输入框、按钮、协议勾选这些可点击的东西;最上面是反馈层,包括 toast、loading、错误提示。分层的好处是,之后无论改背景还是改按钮动效,你都知道该动哪一块,不会牵一发动全身。
用模板结构表达出来大概是这样:
<template> <view class="login-page"> <!-- 背景层 --> <view class="bg-layer"> <view class="blob blob-1"></view> <view class="blob blob-2"></view> </view> <!-- 卡片层 --> <view class="card"> <view class="header">...</view> <!-- 交互层 --> <view class="form">...</view> </view> <!-- 反馈层由 uni.showToast 等承担 --> </view> </template>这套分层看起来有点"为了分层而分层",但它解决了一个真实问题:绝对定位元素的层级冲突。背景的装饰圆、卡片的阴影、输入框的聚焦边框,如果不按层管理 z-index,很容易出现输入框被背景盖住、或者阴影压到按钮上的情况。我吃过一次亏,背景光斑的 z-index 写高了,结果整个表单点不动,排查了半小时才发现是层级问题,从那以后就固定按这四层来写。
2. 视觉层拆解:配色、层次与"高级感"的来源
"高级感"这个词很虚,但拆开看其实是可以量化的:低饱和度、克制的渐变、统一的圆角系统、有呼吸感的留白,再加上一两个点睛的动效。这一章我把这几件事逐个参数化,你照着调数值就能得到差不多的效果。
2.1 配色方案与渐变背景的具体写法
先说配色。登录页最忌讳两种极端:一是纯白背景配纯黑字,像文档;二是搞一堆高饱和渐变色,像十年前的 PPT。我的经验是主色用品牌色,但饱和度压到 60%~70%,背景用主色的极浅色做渐变,文字用深灰而不是纯黑。举个具体的:如果品牌主色是#4A7CFF,那背景可以做成从#EEF3FF到#F8FAFF的斜向渐变,文字用#1A1D26,辅助文字用#8A8F99。
背景的装饰光斑我用的是大半径圆形加高斯模糊,参数上有个细节:模糊值要和圆的大小成比例,不然小圆模糊过度会糊成一团,大圆模糊不够会露出生硬的边。我一般用圆直径的 25% 左右作模糊值。
.bg-layer { position: fixed; inset: 0; z-index: 0; background: linear-gradient(135deg, #EEF3FF 0%, #F8FAFF 60%, #F3F0FF 100%); overflow: hidden; } .blob { position: absolute; border-radius: 50%; filter: blur(60rpx); opacity: 0.55; } .blob-1 { width: 420rpx; height: 420rpx; background: #9BB6FF; top: -120rpx; right: -100rpx; } .blob-2 { width: 320rpx; height: 320rpx; background: #C6B8FF; bottom: -80rpx; left: -60rpx; }注意:
filter: blur()在微信小程序里的性能开销比 H5 大,尤其是圆特别大的时候。如果你发现低端安卓机上滑动掉帧,可以改成用radial-gradient直接做柔边,视觉差异很小但性能好很多。
这里有个小技巧值得单独说:光斑不要放在表单正后方。它会影响输入框文字的可读性,虽然看着柔和,但用户盯久了会觉得累。我的做法是把光斑推到屏幕对角线的两个角上,中间留出干净区域给卡片。
2.2 卡片质感:圆角、阴影、描边的三角平衡
卡片是登录页的视觉中心,它的"高级感"基本由三个参数决定:圆角、阴影、描边。很多新手只调阴影,调出来总是灰蒙蒙的,问题就出在阴影颜色的选择上。纯黑阴影(rgba(0,0,0,.2))在浅色背景上会显脏,正确做法是用主色的深色版本,透明度压到 8% 左右。
我的常用参数是:圆角32rpx,阴影0 12rpx 40rpx rgba(74,124,255,0.10),再叠一层0 2rpx 8rpx rgba(26,29,38,0.04)做贴地感。为什么叠两层?因为单层大模糊阴影负责"悬浮",单层小模糊阴影负责"贴地",两者叠加的立体感比单纯调大 blur 要真实得多,这是从设计规范里学来的思路。
.card { position: relative; z-index: 1; margin: 160rpx 48rpx 0; padding: 64rpx 48rpx 56rpx; background: rgba(255, 255, 255, 0.92); border-radius: 32rpx; box-shadow: 0 12rpx 40rpx rgba(74, 124, 255, 0.10), 0 2rpx 8rpx rgba(26, 29, 38, 0.04); backdrop-filter: blur(20rpx); }backdrop-filter在小程序里的支持度参差不齐,我一般会在它后面补一个不透明的background兜底,这样即使毛玻璃失效,卡片也是完整可见的。别小看这个兜底,我用的是老款安卓测试机,毛玻璃直接不生效,如果没有兜底整个卡片会变成半透明灰,非常难看。
圆角这块还有一个容易忽略的点:卡片圆角和内部输入框圆角要有比例关系。我习惯卡片 32rpx、输入框 20rpx、按钮 20rpx,形成 1.6 倍左右的层级差。如果三个都写 32rpx,界面会显得很"钝",缺少层次。
2.3 微动效:按钮态、输入聚焦、加载过渡
动效是区分"模板"和"作品"的分界线,但登录页的动效必须克制,一个页面塞三种以上的动画就会显得吵。我一般只留三处:按钮按下反馈、输入框聚焦时边框和图标变色、提交时的加载旋转。
按钮反馈用 CSS:active就够了,缩放 0.97 加透明度 0.9,比写 JS 监听 touchstart 省事得多。聚焦态要小心,小程序里 input 的 focus 事件在小键盘弹起时可能触发多次,所以状态改变要用一个布尔值加判断,而不是直接 toggle:
const focusedField = ref('') function onFocus(field) { focusedField.value = field } function onBlur() { focusedField.value = '' }这样写的好处是,无论 focus 事件触发几次,状态都是幂等的,不会出现"聚焦了两次反而取消聚焦"的诡异现象。这个坑我在第三篇里踩过,当时以为是组件库的 bug,查了半天源码才发现是自己状态写错了。
加载过渡我推荐用按钮内部的小圆环旋转,而不是全屏 loading。原因是全屏 loading 会遮住整个页面,用户看不到自己填了什么,心理上会觉得"卡住了"。按钮内联 loading 则传递出一个明确的信号——"你的操作正在处理",体验上从容很多。
<button class="submit-btn" :disabled="loading" :loading="loading" @click="handleSubmit" > {{ loading ? '登录中' : '登 录' }} </button>微信小程序原生 button 的loading属性会自带一个转圈图标,省得自己画。但它有个限制:默认 loading 图标是白色的,如果你的按钮是浅色背景就看不见了。所以要么按钮用深色,要么自己写一个 view 来模拟,我一般选前者,简单可靠。
3. 交互与逻辑:把表单做得既好看又好用
界面好看只是第一步,真正决定用户会不会骂你的,是填表单的过程顺不顺手。这一章聊的全是"细节活",每一条都来自实际项目里被投诉过的点。
3.1 输入框的状态管理与即时反馈
输入框要管的状态其实有四种:默认、聚焦、已填写、错误。很多人只写默认和聚焦两种,结果用户填完内容后没有任何视觉确认,会下意识怀疑"到底输进去了吗"。我的做法是已填写状态下给一个极淡的主色背景,错误状态下边框变红并在下方显示提示文案。
手机上输入时最容易出问题的是清空按钮的显隐。什么时候显示那个小叉?我的判断条件是"当前字段有值且处于聚焦状态",两个条件同时满足才显示。如果只有值就显示,用户在滚动页面时满屏都是小叉,很乱;如果只有聚焦就显示,会有个空叉子,点了也没反应,更莫名其妙。
<view class="field" :class="{ 'is-focus': focusedField === 'phone' }"> <input v-model="form.phone" type="number" maxlength="11" placeholder="请输入手机号" placeholder-class="ph" @focus="onFocus('phone')" @blur="onBlur" /> <view v-if="form.phone && focusedField === 'phone'" class="clear-btn" @click="form.phone = ''" >×</view> </view>这里的placeholder-class值得说一下。小程序里 placeholder 的样式在部分机型上优先级很高,直接写 CSS 选择器可能覆盖不掉,必须用placeholder-class指定一个类名才行。这个细节官方文档里有,但很多人翻不到,导致 placeholder 颜色一直改不了。
3.2 表单校验策略:什么时候校验比怎么校验更重要
校验这件事,时机比规则重要得多。最常见的错误写法是每次输入都校验一次,用户刚输第一个数字就弹"手机号格式不正确",体验极差。我用的策略是分三级:
- 输入过程中只做长度限制(maxlength),不做格式校验;
- 失焦时做一次格式校验,错误就提示;
- 提交时做全量校验,第一个错误字段滚动到可视区域并聚焦。
这套策略的核心逻辑是"不打断用户输入"。用户打字的时候思路是连贯的,你在中间插一个红字提示,他下次输入就会犹豫。等到他手指离开输入框,说明这一段输入完成了,这时候提示才有意义。
手机号的校验规则我用的是/^1[3-9]\d{9}$/,验证码用/^\d{6}$/。这两个正则看起来简单,但有个细节:不要用\d{11}直接匹配所有数字,因为 12 开头的号码段是不存在的,用户输错了你还放行,最后在服务端才报错,白跑一趟网络请求。前端能挡掉的错误就挡掉,是登录页的基本素养。
3.3 按钮加载态与防重复提交的两道保险
防重复提交这件事,我见过太多项目只做了一半。UI 层面按钮置灰是最基础的,但手指点击的速度可能快于状态更新的速度,也就是说用户在loading变成 true 之前已经点了第二下。第一道保险是:disabled="loading",第二道保险必须在 JS 里:
const loading = ref(false) async function handleSubmit() { if (loading.value) return // 第二道保险 if (!validateAll()) return loading.value = true try { await doLogin() } finally { loading.value = false // 无论成功失败都复位 } }finally这一行特别重要。如果登录接口报错,你的loading卡在 true,用户会发现按钮永远点不动,只能退出重进。我统计过一次线上反馈,相当一部分"小程序卡死"其实是 loading 没复位导致的,跟性能没关系。
3.4 协议勾选的合规与交互细节
登录页底部的"用户协议与隐私政策"勾选,是合规要求,也是体验细节。我的处理原则有三条:默认不勾选、点击整行都能切换、文案里的协议名用主色且可点击跳转。
默认不勾选这条现在已经是硬性常识了,但很多人还是习惯写checked: true,这在审核时是有风险的。点击区域这块,一定要把 checkbox 和文案包在同一个可点容器里,只让那个小方块能点的话,手指准确的命中率其实不高,用户会反复点几次才成功。至于文案跳转,用@click.stop阻止冒泡,避免点协议名的时候顺带把勾选也切换了。
<view class="agreement" @click="agreed = !agreed"> <view class="checkbox" :class="{ checked: agreed }"></view> <text class="text">我已阅读并同意</text> <text class="link" @click.stop="openProtocol('user')">《用户协议》</text> <text class="text">和</text> <text class="link" @click.stop="openProtocol('privacy')">《隐私政策》</text> </view>实操心得:checkbox 的勾选对勾不建议用特殊字体图标,直接用 CSS 边框旋转 45 度画一个勾最稳,跨端表现一致,也不依赖字体文件加载。
4. 登录链路的完整落地
视觉和交互都到位之后,就是真正的数据流了。这一章是整篇的硬核部分,也是很多教程一带而过的地方。
4.1 code 换 session 与业务 token 的正确分工
微信小程序的登录流程经常被讲混,我用一句话概括:uni.login拿到的是"入场券",它换来的 session 是"临时身份",真正在业务里横着走的是你自己服务端签发的 token。这三者不能互相替代。
完整链路是这样的:用户点登录按钮,前端调用uni.login拿到code,把code连同手机号、验证码一起发给自己的服务端;服务端拿code去微信的接口换openid和session_key,然后生成自己的业务 token 返回;前端把 token 存起来,后续所有请求带上它。
async function doLogin() { const { code } = await new Promise((resolve, reject) => { uni.login({ provider: 'weixin', success: resolve, fail: reject }) }) const res = await uni.request({ url: `${BASE_URL}/auth/login`, method: 'POST', data: { code, phone: form.phone, smsCode: form.smsCode } }) if (res.data.code !== 0) { throw new Error(res.data.message || '登录失败') } uni.setStorageSync('token', res.data.data.token) uni.setStorageSync('userInfo', res.data.data.userInfo) uni.reLaunch({ url: '/pages/index/index' }) }这里有个关键点:code是一次性的,用完即失效。如果你的接口报错需要用户重试,必须重新调一次uni.login拿新的 code,不能复用旧的。我遇到过一次线上问题,用户第一次登录失败后重试总是失败,排查了两小时才发现是 code 被复用了,改成每次提交都重新获取就正常了。
4.2 token 存储与请求拦截
token 存哪里?uni.setStorageSync存在本地,简单直接,但要注意它是明文存储的。小程序环境相对封闭,风险可控,但我不建议把敏感信息一起存进去。我一般只存 token 和必要的用户基础信息,像身份证号、完整手机号这类,需要时再单独请求。
请求拦截这块,如果项目里用了uni.request的封装或者第三方请求库,记得在请求头里统一注入 token,并且处理401 的场景——token 过期时清空本地存储并跳回登录页,而不是让用户在所有页面里看到一堆报错。
uni.addInterceptor('request', { invoke(args) { const token = uni.getStorageSync('token') if (token) { args.header = { ...args.header, Authorization: `Bearer ${token}` } } }, success(res) { if (res.statusCode === 401) { uni.removeStorageSync('token') uni.reLaunch({ url: '/pages/login/login' }) } } })注意:拦截器里跳转登录页前,最好判断一下当前页面是不是已经是登录页,否则极端情况下会出现重复跳转的堆栈异常。
4.3 图形验证码与短信验证码的接入姿势
验证码分两种,短信验证码是发给手机号的一次性数字,图形验证码是人眼识别的字符或滑块。前者用于登录验证,后者通常用于防止短信接口被刷。实际项目里常见的是两者配合:点"发送验证码"之前,先弹一个图形验证码,验证通过才真的发短信。
前端接入图形验证码的流程是:页面加载时请求一次验证码图片(服务端返回图片和对应的标识 key),用户输入后连同 key 一起提交验证。这里要处理两个细节:验证码过期要能刷新,所以图片上加一个点击刷新的事件;提交失败后验证码一般会失效,需要自动刷新一次,否则用户拿着旧图反复输都过不了。
<view class="captcha-row" v-if="needCaptcha"> <input v-model="form.captcha" placeholder="请输入图形验证码" /> <image :src="captchaImg" @click="refreshCaptcha" class="captcha-img" /> </view>发送短信的按钮要有 60 秒倒计时,而且倒计时状态要能在页面隐藏再回来之后恢复正确。简单用一个setInterval在onUnload里清理就够了,但如果用户中途切到后台,小程序的定时器可能被挂起,回来时倒计时对不上。更稳的做法是记录一个结束时间戳,每次渲染时用当前时间减去它算剩余秒数,这样切后台再回来也不会错乱。
顺便提一句,验证码校验一定放在服务端做,前端的所有校验都只是提升体验,不能当作安全手段。这个原则适用于所有登录相关的场景,我在代码评审里看到过前端直接判断验证码等于某个固定值的写法,那是绝对不行的。
5. 适配与避坑:多端差异的真实表现
uni-app 最大的价值是跨端,最大的坑也是跨端。登录页作为纯 UI 页面,看起来应该很好统一,实际上微信小程序端和 H5 端、App 端的差异还是会冒出来。这一章我按实际踩过的顺序讲。
5.1 rpx 与 px 混用的边界在哪里
rpx是小程序的响应式单位,标准是 750rpx 等于屏幕宽度。用它写尺寸,在不同机型上会自动等比缩放,这很好,但不是所有地方都该用 rpx。我的经验是:字号、间距、圆角、图标大小用 rpx;1px 的细线、阴影的偏移量和模糊值用 px。
为什么细线要用 px?因为如果用 rpx,在 2 倍屏上 1rpx 换算出来是 1px 的物理像素,看起来会粗细不均。阴影同理,用 rpx 写模糊值在低分辨率设备上会糊得不成样子。至于字号,有个下限要注意,24rpx在小屏手机上已经接近看不清了,正文建议不低于26rpx。
还有一个容易忽略的地方:卡片宽度不要写死690rpx。虽然 750 减去左右各 30 的边距确实是 690,但用户在平板上打开时,整个卡片会被拉伸得特别宽,阅读体验很差。可以给卡片加一个max-width,在宽屏上居中显示,观感会好很多。
5.2 键盘弹起与安全区的处理
键盘弹起导致的页面错位,是登录页最经典的坑。微信小程序的 input 有adjust-position属性,默认是 true,也就是键盘弹起时页面会自动上推。多数情况下这个默认行为就够了,但如果你的登录卡片是垂直居中的,上推之后卡片会被顶出屏幕,顶部内容看不见。
我的方案是把卡片改成顶部对齐加固定上边距,而不是垂直居中。虽然视觉上稍微没那么"完美居中",但键盘弹起时不会有任何跳动,用户视野稳定。如果你坚持居中,那就得监听focus时手动调整 margin,代码量翻倍还不稳定,不划算。
安全区的问题主要出现在全面屏底部。底部的协议文案如果贴着屏幕最下方,在带虚拟指示条的机型上会被挡住。用padding-bottom: env(safe-area-inset-bottom)可以解决,但要注意 uni-app 编译到小程序时这个 CSS 变量在部分基础库版本上不生效,稳妥起见我会额外加一个固定值的兜底 padding。
.login-page { padding-bottom: calc(40rpx + env(safe-area-inset-bottom)); padding-bottom: calc(40rpx + constant(safe-area-inset-bottom)); /* 老版本兜底 */ }5.3 微信小程序端特有的几个注意点
有几个差异只在微信小程序端出现,H5 上完全正常,很容易被忽略。
第一个是页面栈限制。小程序的页面栈最多十层,登录成功后如果用navigateTo跳首页,用户按返回键会回到登录页,这个体验是错的。所以登录成功后必须用reLaunch或者redirectTo,把登录页从栈里清掉。这一点我在第四章的代码里已经体现出来了。
第二个是输入框的原生层级问题。微信小程序的 input 在早期版本里是原生组件,层级最高,会盖住所有普通 view。虽然现在的新版基础库已经改善了很多,但如果你在输入框上方做浮层,还是要留意。我遇到过弹窗被输入框盖住的情况,最后的解决方案是用cover-view,或者干脆把弹窗位置挪开。
第三个是基础库版本差异。有些新属性在老版本基础库上不生效,比如backdrop-filter。稳妥的做法是功能降级,能用则用,不能用也不影响主体功能。可以在小程序后台设置最低基础库版本,但设置得太高会流失一部分老设备用户,这个权衡得看你的用户画像。
6. 性能优化与常见问题速查
6.1 首屏渲染与资源体积控制
登录页通常是用户进入小程序后看到的第一个页面,它的首屏速度直接影响留存。这块我做了三件事:背景用纯 CSS 画不用图片、图标用字体或者 SVG 内联不用 PNG、首屏不加载任何非必要的 JS。
用 CSS 画背景的好处前面说过,性能好而且没有网络请求。图标这块,如果项目里已经用了图标字体,直接复用;如果没有,小图标建议内联 SVG 或者用 CSS 画,一个 PNG 图标动辄几 KB,七八个就是几十 KB,在弱网环境下就是几秒的差距。至于 JS,登录页不需要引入状态管理库的全量代码,按需引入或者干脆不用,一个页面的局部状态用ref管就够了。
还有一个小优化点:输入框的type属性要写对。手机号写type="number",验证码也写type="number",这样键盘会自动弹出数字键盘,用户少切一次键盘,体验和速度都提升了。密码字段如果要显示/隐藏切换,用:password属性控制,而不是切换 type,后者在部分机型上会导致光标位置重置。
6.2 输入卡顿的排查思路
"输入框打字卡顿"是高频反馈,但原因往往不在输入框本身。我按可能性从高到低列一下排查顺序:
| 排查项 | 典型现象 | 处理方式 |
|---|---|---|
| 输入事件里有重计算 | 每打一个字就卡一下 | 用防抖,或者把校验移到失焦 |
| 大范围动态样式绑定 | 滚动或输入时整体掉帧 | 减少响应式依赖的节点数量 |
| 背景模糊滤镜 | 低端机持续掉帧 | 改用渐变模拟,或降低模糊半径 |
| 页面元素过多 | 首屏和输入都卡 | 精简 DOM,隐藏元素用 v-if |
| 长列表在同一个页面 | 输入时页面重排 | 拆分成独立页面或虚拟列表 |
我遇到最多的是第一项。有人写了个watch监听输入内容,每次变化都去请求接口查手机号是否存在,结果每按一个键就发一次请求,别说卡顿,流量都浪费了。这种需求应该改成失焦后再查,或者加 500 毫秒防抖。
6.3 常见问题速查表
下面这张表是我攒了三年的"登录页翻车记录",基本覆盖了你能遇到的大部分情况:
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 点击登录没反应 | loading 未复位或事件未绑定 | 加 finally 复位,检查 @click |
| 提示"登录失败"但无详情 | 错误信息被吞 | 统一错误处理,打印服务端 message |
| 第二次登录必失败 | code 被复用 | 每次提交重新调 uni.login |
| 退出登录后仍显示用户信息 | 缓存未清理 | 退出时清空所有 storage 并 reLaunch |
| 输入框 placeholder 颜色改不了 | 未使用 placeholder-class | 加 placeholder-class 属性 |
| 安卓机上面包屑被键盘遮挡 | 页面垂直居中 | 改为顶部对齐布局 |
| iOS 上点击有延迟 | 未使用 :active 或用了 touch | 用 CSS 伪类替代 JS 事件 |
| 验证码按钮倒计时错乱 | 定时器切后台被挂起 | 改用时间戳计算剩余秒数 |
| 卡片在不同机型宽度不一 | 写死宽度 | 用百分比加 max-width |
| 深色模式下界面发白 | 未适配深色模式 | 用 CSS 变量或 media 查询 |
这张表里我最想强调的还是第一条和第三条,因为它们最隐蔽,排查起来最费时间。尤其是 code 复用那个问题,现象是"偶尔失败",很容易被当成网络问题放过去,实际上它必然会发生,只是频率不高。
7. 我在实际项目里踩出来的几条经验
写了这么多技术细节,最后聊几条不那么技术、但同等重要的经验。
第一条是别在登录页堆功能。我见过一个项目,登录页上塞了手机号密码登录、微信一键登录、账号密码、游客体验四种方式,还带一个轮播的活动 banner。结果页面长得吓人,用户第一眼不知道点哪个。登录页的核心任务只有一个:让用户用最快的方式进来。如果真的需要多种方式,也应该是主次分明,一种主推、其余藏在"更多方式"里。
第二条是把错误提示写清楚。"登录失败"这四个字等于没说,用户不知道是自己手机号错了,还是验证码过期了,还是网络断了。我现在养成的习惯是,服务端返回的message尽量直接展示给用户,实在不适合展示的再兜底一个通用文案。用户骂人的次数会明显减少。
第三条是登录页要能自己一个人测试。什么意思呢,就是不要让它强依赖某个只有正式环境才有的接口。我会在开发阶段用 mock 数据把整个链路跑通,包括成功、验证码错误、网络超时三种情况,确保每种情况下的 UI 表现都正常。等接真接口的时候,只需要替换请求层,页面代码一行都不用改。
第四条关于设计走查。功能做完之后,我习惯拿三台设备过一遍:一台小屏安卓、一台常规尺寸 iPhone、一台平板或者折叠屏。这三个尺寸基本能覆盖所有布局问题。特别是折叠屏,展开之后屏幕很宽,如果你的卡片没有 max-width,视觉效果会非常奇怪,这个在正常手机上完全看不出来。
这套登录页的代码我前后迭代了四次,从最初的能跑就行,到现在的细节打磨,每次都是在真实用户的反馈里改出来的。视觉上的"好看"其实是最容易的部分,照着参数调就能出效果;难的是那些看不见的地方,比如状态复位、错误处理、跨端差异。把这两者都做扎实了,一个登录页才算真的完成。后续如果你要在这个基础上继续扩展,我的建议是往"无密码登录"的方向走,用手机号加短信验证码或者第三方授权替代密码输入,能再砍掉一整个输入框和相关的状态管理,用户的操作路径会短很多,维护成本也跟着降下来。