☰
uni-app 微信小程序登录页第三版:三态输入框与真机适配
2026/9/29 22:21:56 网站建设 项目流程

1. 第三版登录页的基调:先解决"前两版为什么看着不难看却不好用"

这是 uni-app 微信小程序登录页系列的第三篇。前两篇分别做的是渐变背景版和插画风版,评论区里问得最多的其实不是"怎么写出这个渐变",而是"我这页面在真机上怎么跟你的不一样"。所以第三篇我打算换个思路:先把设计基调定下来,再动手写样式,最后专门讲那些在模拟器里看不出来、一上真机就翻车的地方。

这一版的登录页面定位很明确,面向的是工具类、服务类小程序的主入口。它的核心诉求不是视觉冲击,而是让用户在两秒内知道该点哪里、该填什么,并且愿意把手机号交出来。关键词里的 uni-app、微信小程序、UI、登录页面,落到代码上其实就是三件事:一套可复用的样式变量、一套能覆盖各种状态的输入框组件、一套在低端安卓机上也不掉帧的动效方案。

适合谁看?如果你已经能用 uni-app 跑通一个页面,但对"为什么我的圆角看起来跟设计师稿子不一样""为什么键盘弹起来页面就乱"这类问题还没有稳定答案,那这篇基本能对上你的痛点。代码我尽量给全,但更想讲清楚每个数值为什么是那个数。

1.1 前两版在真机上暴露的三个问题

第一版用了大面积的线性渐变背景,在模拟器上非常好看,但在部分千元安卓机上,linear-gradient配合border-radius的大面积渲染会出现明显的色带,尤其是从深蓝过渡到紫色这一段,肉眼可见的横条纹。原因不复杂,低端机的屏幕色深和 GPU 渲染精度不足,渐变步进被量化了。

第二版换成了插画背景图,问题变成了包体积。一张 750x1624 的 PNG 压缩到 200KB 已经是极限,再加暗黑模式要两套图,直接 400KB 起步。而这张图对首屏渲染的阻塞效应很明显,用户点进来先看到一片白,再跳出图。

第三个问题是两版都有的:输入框用了原生input的border直接描边,聚焦的时候只改border-color。这种写法在 iOS 上没问题,但在部分安卓机型上,光标定位到输入框时页面会有一个轻微的横向抖动,因为边框宽度的变化触发了重排。

1.2 把设计变量先定死再写样式

第三版我做的第一件事,是把所有会重复使用的数值抽成 SCSS 变量,放在一个独立的文件里。这不是为了显得规范,而是因为登录页的元素虽少,但状态多——默认、聚焦、错误、禁用,每种状态都要配色,如果颜色散落在各个组件里,改一次主题要翻五个文件。

// styles/_variables.scss $c-bg: #F6F7FB; // 页面底色,低饱和冷灰 $c-surface: #FFFFFF; // 卡片/输入框底色 $c-text-main: #1A1D26; // 主文字 $c-text-sub: #8A8F9C; // 占位符、辅助文字 $c-line: #E8EAF0; // 分割线、默认描边 $c-primary: #3B6FFF; // 唯一强调色 $c-primary-weak: rgba(59, 111, 255, 0.08); $c-danger: #F5483B; $radius-card: 32rpx; $radius-field: 24rpx; $gap-field: 28rpx; $shadow-card: 0 8rpx 40rpx rgba(26, 29, 38, 0.06);

这里有个取值逻辑值得说一下。页面底色我选了#F6F7FB而不是纯白,原因是登录页整体是大面积浅色,如果背景和卡片都是纯白,卡片只能靠阴影区分,而小程序的阴影渲染在低端机上偏灰偏脏,边缘容易糊。给背景压一点点灰度,卡片的白就会自己浮起来,阴影只需要很轻的一层就够。

强调色只保留一个#3B6FFF。登录页最忌讳的是颜色打架——按钮一个色、链接一个色、验证码一个色,用户视线被撕成三份。我把所有需要"被点击"的元素统一成这个蓝,其他一律走灰阶。

1.3 页面结构与文件组织

登录页我不建议写成一个几百行的单文件,但也不建议拆成十几个自定义组件,中间有个平衡点。小程序端每个自定义组件都会带来一次独立的数据通信,拆得太碎,在低端机上光是组件初始化就要几十毫秒。

我的做法是按目录分层:

pages/login/ ├── index.vue // 页面容器,只管布局与业务逻辑 └── components/ ├── login-field.vue // 输入框,三态合一 ├── login-button.vue // 主按钮,含加载/禁用态 └── agree-bar.vue // 协议勾选条

四个文件,页面本身负责数据与请求,三个组件都是纯展示。这样既保证了输入框逻辑复用,又把组件树深度控制在两层以内。超过三层嵌套在小程序端就会出现明显的样式穿透困难和事件冒泡问题,尤其是catchtap和bindtap混用的时候。

2. 页面骨架拆解:背景层、品牌层、表单层、协议层各干什么

很多人写登录页的习惯是从上往下堆元素:先来个 logo,再来两个 input,最后放个按钮。这样写出来的页面能用,但改起来很痛苦,因为元素之间没有明确的职责边界,调一个间距会牵动三个地方。

我的习惯是先把页面切成四层,每层只负责一件事。这四层不是四个组件,而是四个视觉区域,用容器包起来就行,但它们之间的层级关系必须提前想清楚。

2.1 四层的职责与层级关系

  • 背景层:铺满整个视口,负责氛围。这一版我用的是纯色底 + 顶部一块柔和的光晕,不再用整屏渐变,规避前面提到的色带问题。
  • 品牌层:logo、欢迎语、副标题。它的高度在小屏上要能压缩,所以里面所有尺寸都用相对单位。
  • 表单层:输入框、主按钮、辅助链接。这是页面的重心,视觉上要最"实"。
  • 协议层:勾选框与协议文本。位置固定在表单下方,不参与滚动。

层级上用z-index区分,但小程序里更稳妥的方式是直接用文档流顺序,只有背景层用position: fixed脱离文档流。

<template> <view class="login-page"> <view class="glow" /> <view class="brand"> <image class="logo" src="/static/logo.png" mode="aspectFit" /> <text class="title">欢迎回来</text> <text class="subtitle">登录后继续你的工作台</text> </view> <view class="form-card"> <login-field v-model="phone" type="phone" placeholder="请输入手机号" /> <login-field v-model="code" type="code" placeholder="请输入验证码" /> <login-button :loading="loading" :disabled="!canSubmit" @tap="handleSubmit"> 登 录 </login-button> </view> <agree-bar v-model="agreed" /> </view> </template>

背景光晕那块我用的是radial-gradient配合较大的blur,位置固定在顶部偏左,让视觉重心自然落在左上的品牌层和中间的表单上。这里要注意filter: blur()在小程序端支持度一般,稳妥的做法是用一张极小的模糊图片拉伸,或者干脆用radial-gradient本身做柔和过渡,不加 blur。

2.2 状态栏与胶囊按钮的避让,别让标题顶到刘海里

这是真机上最容易翻车的点。微信小程序的右上角有胶囊按钮,自定义导航栏的页面必须自己算高度,否则要么标题被胶囊压住,要么留白大得离谱。

标准算法是这样:

const sys = uni.getSystemInfoSync() const capsule = uni.getMenuButtonBoundingClientRect() // 胶囊上边距与状态栏之间的间距,上下对称 const navBarHeight = (capsule.top - sys.statusBarHeight) * 2 + capsule.height this.statusBarHeight = sys.statusBarHeight this.navBarHeight = navBarHeight

拆开看这个公式:capsule.top - sys.statusBarHeight就是胶囊顶部到状态栏底部的距离,这段距离在视觉上应该是上下对称的,所以乘 2,再加上胶囊自身高度,就是整个导航栏的高度。这样算出来的导航栏,标题在垂直方向正好和胶囊居中对齐,不会偏。

如果你不做自定义导航栏,那就要在页面顶部留出statusBarHeight的高度,否则内容会被状态栏遮住。这个值在 iOS 上通常是 44,安卓上从 20 到 48 都有,千万别写死。

2.3 表单卡片的圆角、内边距与阴影取值依据

卡片我用了32rpx的圆角。这个数字不是随便定的。微信小程序的视觉规范里,卡片类容器常见圆角在 24rpx 到 40rpx 之间。低于 24rpx 会显得硬,高于 40rpx 在小屏上会侵占内容空间,而且和输入框的圆角拉不开差距。

输入框我用24rpx,比卡片小一档。这里有个通用规律:嵌套层级越深,圆角越小。卡片 32、输入框 24、按钮 24,视觉上就形成了从外到内的递进关系。如果输入框也用 32,整张卡片看起来会像一堆圆角矩形堆在一起,很散。

内边距我用的40rpx左右、36rpx上下。这个值的依据是:输入框本身高96rpx,加上上下内边距,卡片在表单区的高度大约在400rpx出头,占一屏高度的三分之一左右,视觉上比较舒服。如果内边距压到 24rpx,卡片会显得局促;如果放到 56rpx,小屏手机上卡片会顶到协议层。

阴影那行0 8rpx 40rpx rgba(26, 29, 38, 0.06),横向偏移 0,纵向 8rpx,模糊 40rpx,颜色是主文字色加了 6% 透明度。这里的关键是阴影颜色不要用纯黑,纯黑阴影在浅色背景上会发灰发脏。用主文字色降透明度,阴影会自然融入环境色,看起来更透气。

3. 输入框组件的三态设计:默认、聚焦、错误

登录页的灵魂其实是输入框。用户 90% 的操作时间都在跟它打交道,按钮只是最后一下。所以输入框的状态设计值得单独拿出来讲。

3.1 为什么不用原生 input 自带的边框

原生input组件的border属性是直接作用在输入区域上的,看起来最省事,但会带来两个问题。一是聚焦时改变边框颜色,边框宽度若有细微变化会触发布局重排,部分安卓机型上表现为页面轻微抖动;二是无法做出"图标 + 输入区 + 右侧操作按钮"这种复杂结构,因为边框会把这些都围进去。

我的做法是把边框画在外层容器上,input本身设置border: none,透明背景,占满剩余空间。

<template> <view class="field" :class="fieldClass"> <view class="field__icon"> <text class="iconfont">{{ iconMap[type] }}</text> </view> <input class="field__input" :value="modelValue" :type="inputType" :password="type === 'password' && !visible" :maxlength="maxlength" :placeholder="placeholder" placeholder-class="field__placeholder" :adjust-position="false" @input="onInput" @focus="focused = true" @blur="focused = false" /> <view v-if="showClear && modelValue" class="field__clear" @tap="handleClear"> <text class="iconfont">✕</text> </view> </view> </template>

adjust-position设成false是个关键决定,后面第 5 节会详细讲。这里先记住:一旦关掉系统自动上推,键盘弹起时页面上移就得自己控制,但换来的是完全可控的布局,不会出现"键盘推一半又弹回去"的鬼畜现象。

3.2 三态切换的类名逻辑与实现

三态我通过一个计算属性统一管理,避免在 template 里堆一堆三元表达式。

computed: { fieldClass() { return { 'field--focus': this.focused && !this.errorMsg, 'field--error': !!this.errorMsg, 'field--filled': !!this.modelValue && !this.focused } } }

样式上,默认态描边用1px solid $c-line,聚焦态换成$c-primary并加一层$c-primary-weak的淡色底,错误态换成$c-danger。

.field { display: flex; align-items: center; height: 96rpx; padding: 0 28rpx; background: $c-surface; border: 1px solid $c-line; border-radius: $radius-field; transition: border-color 0.18s ease, background-color 0.18s ease; &--focus { border-color: $c-primary; background: $c-primary-weak; } &--error { border-color: $c-danger; animation: shake 0.3s ease; } }

注意这里用的是border而不是outline,并且没有改变边框宽度,只改颜色。这一个细节就能避免前面说的重排抖动。1px 的边框在小程序里不会因为缩放而变得模糊,这一点比 web 端省心。

3.3 手机号、验证码、密码框的差异化处理

三种输入框看着一样,实际上细节差别不小。

类型input typemaxlength附加能力注意事项
手机号number11清空按钮安卓部分机型 number 键盘无小数点,可用
验证码number6倒计时按钮必须限制长度,否则短信接口会报错
密码text20显隐切换用 password 属性而非 type,兼容性更好

密码框这里有个坑要单独说。uni-app 的input组件有两个控制密码显示的属性:type="password"和:password="true"。在小程序端,推荐用password布尔属性配合type="text",因为type="password"在部分基础库版本下,切换显隐时需要重新挂载节点才能生效,表现为点一下眼睛图标没反应,再点一下才变。用:password属性切换,是即时的。

验证码框我建议切成六个独立小格子的样式吗?不建议。那种六格输入在登录页会增加不少实现复杂度,而且粘贴、退格的处理容易出 bug。一个普通输入框加右侧倒计时按钮,用户认知成本最低。真正需要六格的是支付密码那种高安全场景。

3.4 清空按钮、密码显隐与验证码倒计时

清空按钮的实现很直接,但有个细节:点击时要注意不要触发输入框的 blur 逻辑导致误判。我把它包在@tap.stop里,阻止冒泡。

倒计时用setInterval就够,但要记住两件事:一是在onUnload里清掉定时器,否则用户登录成功跳转后定时器还在跑,返回页面时数字是乱的;二是倒计时期间按钮要变成不可点状态,并且文本要变灰,不然用户会反复点。

startCountdown() { this.countdown = 60 this.timer = setInterval(() => { this.countdown-- if (this.countdown <= 0) { clearInterval(this.timer) this.timer = null this.countdown = 0 } }, 1000) }, onUnload() { if (this.timer) clearInterval(this.timer) }

这里有个我踩过的坑:60 秒倒计时不要用v-if控制按钮的显示隐藏,要用:disabled加类名切换。因为v-if会让节点重建,在某些机型上按钮宽度会因为文字长短变化而跳一下,视觉上不舒服。

4. 主按钮、协议勾选与第三方登录的排版细节

4.1 主按钮的禁用态、加载态与防重复点击

主按钮最核心的功能不是好看,是防止用户连点。网络慢的时候用户会本能地连点五下,如果没做拦截,就会发出五个登录请求,轻则重复发送验证码,重则账号被风控。

我的做法是三保险:

  1. disabled属性绑到计算属性上,手机号或验证码不合法时直接禁用;
  2. 请求发出后把loading置为true,按钮显示加载状态并拒绝点击;
  3. 在handleSubmit里做一次同步的防抖判断,用实例上的标志位锁住。
async handleSubmit() { if (this.locked || this.loading) return this.locked = true this.loading = true try { await this.doLogin() } finally { this.loading = false // 给一个短暂冷却,避免请求返回过快导致用户二次点击 setTimeout(() => { this.locked = false }, 500) } }

为什么要在finally里再延迟 500ms 才解锁?因为如果接口很快返回(比如 200ms),用户第一下点击的惯性还没结束,第二下就落在按钮上了,这时候按钮已经恢复可点,就会重复提交。加个 500ms 冷却,覆盖掉大部分人的连点间隔,实测下来很有效。

禁用态的视觉处理也有讲究。不要简单地用opacity: 0.5,那样在浅色底上会显得发灰。我用的是降低饱和度的方式:背景色换成#B9C6E8这类主色的淡化版,文字保持纯白。这样即使禁用,按钮的"存在感"还在,用户知道这里有个可点的目标,只是条件没满足。

加载态用一个小的旋转圈,放在文字左侧,圈和文字之间留16rpx。注意加载圈的动画要用transform: rotate,不要用background-position做动画,后者在小程序里会触发重绘,低端机上肉眼可见的卡。

4.2 协议勾选的交互边界

协议勾选条的位置我放在表单卡片外面、页面底部。理由是它属于"法律信息",不属于"操作区",放在卡片里会稀释表单的聚焦感。

交互上有几个边界要处理清楚:

  • 勾选框本身要够大:视觉尺寸36rpx,但点击热区要扩到88rpx,用 padding 撑开。手指点击精度在 44px 左右,小于这个值误触率会明显上升。
  • 点文字也能勾选:用户不会精准地点那个小方块,把整行做成可点击区域。
  • 点击协议链接时不要切换勾选状态:链接要用@tap.stop,否则用户想看协议,结果勾选被切换了,体验很糟。
  • 未勾选时点登录:不要直接禁用按钮,而是允许点击,然后弹出一个轻提示并把勾选框做一个抖动动画。直接禁用的话用户不知道问题出在哪。
<view class="agree-bar" @tap="toggle"> <view class="agree-bar__box" :class="{ 'is-checked': modelValue }"> <text v-if="modelValue" class="iconfont">✓</text> </view> <text class="agree-bar__text"> 我已阅读并同意 </text> <text class="agree-bar__link" @tap.stop="openAgreement('user')">《用户协议》</text> <text class="agree-bar__text">和</text> <text class="agree-bar__link" @tap.stop="openAgreement('privacy')">《隐私政策》</text> </view>

勾选框的选中动画我用了transform: scale从 0.8 弹到 1,配合cubic-bezier(0.34, 1.56, 0.64, 1)这个带轻微回弹的缓动。这个曲线是我试了很多次调出来的,太弹会显得幼稚,不弹又太死板,这个参数在小程序里表现刚好。

4.3 第三方登录区的间距与分割线

如果登录页要放第三方登录(微信一键登录、手机号快捷登录),位置一定在协议条之上、主按钮之下,中间用分割线隔开。

分割线我用的不是纯 HTML 的hr,而是"左右两条线 + 中间文字"的结构:

.divider { display: flex; align-items: center; margin: 48rpx 0 32rpx; color: $c-text-sub; font-size: 24rpx; &::before, &::after { content: ''; flex: 1; height: 1px; background: $c-line; } &::before { margin-right: 24rpx; } &::after { margin-left: 24rpx; } }

::before和::after用flex: 1平分剩余空间,这样不管中间文字多长,两条线都是等长的,不会一边长一边短。这个小技巧比手动算宽度靠谱得多。

第三方按钮我用的是图标 + 文字的组合,横向排列,图标尺寸48rpx,按钮之间的间距64rpx。间距之所以给这么大,是因为这类按钮通常是圆形的,视觉重量轻,间距小了会挤成一堆。三个以上的话建议改成一行均分,超过四个就要考虑折叠到"更多"里了。

5. 动效与性能:登录页最容易被忽略的两处开销

登录页看起来简单,但它是那种"动一下就卡"的页面,因为上面全是输入框、光标、键盘、动画。

5.1 用 CSS 动画做抖动,别用 setInterval 驱动

错误提示的抖动效果,网上很多写法是用setInterval每隔 50ms 改一次translateX的值,改个七八次。这种写法在小程序上是大忌,因为每次改数据都会触发一次setData,而setData是小程序里最贵的操作,七八次连续调用足以让低端机掉帧。

正确做法是把动画写进 CSS,只在需要的时候切换一个类名。

@keyframes shake { 0% { transform: translateX(0); } 20% { transform: translateX(-10rpx); } 40% { transform: translateX(10rpx); } 60% { transform: translateX(-6rpx); } 80% { transform: translateX(6rpx); } 100% { transform: translateX(0); } }

整个抖动过程只触发一次setData(加类名),动画由渲染层自己跑。而且位移量是递减的,从 10rpx 到 6rpx,比等幅抖动更自然。

有个细节要注意:同一个类名连续触发两次,第二次可能不生效,因为类名没变,浏览器和渲染层不会重启动画。解决办法是加类名后延迟一小会儿再移除,或者用一个自增的 key 强制节点重建。我用的方式是先移除、在nextTick里再加回来。

5.2 自定义组件的层级与 setData 开销

前面提到我把登录页拆成四个文件,这是有取舍的。小程序的自定义组件每一层都会带来额外的通信开销,父组件给子组件传值,走的是setData的路径。

具体来说,login-field组件内部自己维护聚焦状态、清空逻辑,这些变化不会往上冒泡到页面,所以页面组件的setData数据量很小,只有phone、code、loading这几个字段。如果把所有状态都放在页面里,每次聚焦、失焦、输入都要更新页面的 data,那setData的数据量会翻好几倍。

经验值是:登录页的自定义组件层级别超过两层,页面 data 字段控制在 10 个以内。超了就要考虑把一部分状态下沉到组件内部,或者用props加emit的方式单向传递,别用双向绑定的v-model到处连。

5.3 键盘弹起时页面上移的处理

这个问题在不同机型上的表现千差万别。iOS 上键盘弹起时,如果输入框在屏幕下半部分,系统会把整个 webview 往上推;安卓上大部分机型不推页面,而是把键盘盖在上面,输入框可能被遮住。

前面我提到给input设了:adjust-position="false",关掉系统自动上推。关掉之后,我需要自己控制布局。思路是:监听键盘高度变化,如果输入框的底部位置低于"屏幕高度 - 键盘高度",就把表单容器整体上移相应的距离。

onKeyboardHeightChange(res) { const kb = res.height if (!kb) { this.keyboardOffset = 0 return } // 表单容器距离视口底部的距离 const formBottom = this.formBottomInViewport const visibleBottom = this.windowHeight - kb const diff = formBottom - visibleBottom this.keyboardOffset = diff > 0 ? diff : 0 }

上移的时候要用transform: translateY,不要改margin或top,前者只触发合成,后者触发重排。而且上移量要给一个上限,比如最多上移 200rpx,超出部分就不管了——因为再往上顶,品牌层会飞出屏幕,用户会看到一个空白的顶部,反而更慌。

关掉adjust-position还有一个好处:页面不会因为键盘弹起而整体跳动,输入框的光标位置是稳定的,用户在输入密码时不会出现"光标跑位"的情况。

6. 多机型适配与暗黑模式

6.1 rpx 与 px 的混用规则

rpx 是小程序的响应式单位,750rpx 恒等于屏幕宽度。听起来很美好,但它有个前提:设计稿必须是 750 宽。如果你的设计稿是 375 或者 390 宽度标注的,直接换算成 rpx 就会出问题。

我的规则是:和屏幕宽度相关的一切用 rpx,和外设/系统相关的用 px。比如状态栏高度、导航栏高度、胶囊按钮尺寸,这些是系统给的绝对值,单位就是 px,绝对不能换算成 rpx,否则在 iPad 或折叠屏上会完全错位。

字体大小我用 rpx,但会给一个下限。比如正文28rpx,在小屏机型(宽度 320px 左右)上计算出来约等于 12px,偏小了。所以辅助文字我单独用24rpx并配合min-font-size的思路——实际上小程序不支持这个属性,我的做法是在小屏时通过媒体查询把根字号调大,或者干脆不缩,让文字在小屏上占的比例大一点,可读性反而更好。

6.2 暗黑模式的两套变量

微信小程序支持darkmode,需要在app.json里开启,页面才能响应系统的主题变化。

{ "darkmode": true, "themeLocation": "theme.json" }

然后在theme.json里定义两组变量,在页面里用var(--xxx)引用。但要注意,theme.json里的变量只能用在 wxss 里,不能用在 scss 变量中,所以我的做法是:SCSS 变量负责结构和尺寸,CSS 变量负责颜色。

.login-page { background: var(--page-bg, #F6F7FB); color: var(--text-main, #1A1D26); }

暗黑模式下最容易出问题的是阴影。浅色主题里的阴影到了深色背景上会完全看不见,这时候需要用"描边"来替代"阴影"来区分层级。我给卡片加了一层很淡的1px边框,颜色是半透明白,暗色下卡片就能浮出来了。

另一个坑是图片资源。logo 如果只有一张深色版本,在暗黑模式下贴在深色背景上会糊成一团。要么准备两套图,要么给 logo 外面加一个浅色的圆形底座,让它不管在什么主题下都有对比度。我选了后者,省体积。

6.3 低端机型与小屏的降级

低端机型我做了三处降级:

第一,关掉所有非必要的动画。光晕的呼吸效果、按钮的渐变流动,这些在低端机上统统关掉,只保留输入框的三态过渡和错误抖动这两个有功能意义的动画。

第二,减少圆角。大圆角在低端机上渲染成本更高,尤其是配合阴影的时候。我在低端机上把卡片圆角从 32rpx 降到 16rpx,肉眼差别不大,但渲染压力小一些。

第三,小屏压缩垂直间距。小屏机型(屏幕高度低于 1334 对应的机型)上,品牌层的高度要压缩,logo 从120rpx降到88rpx,副标题可以隐藏,把空间让给表单。

判断方式是在onLoad里读一次系统信息,算出一个isCompact标志,然后挂到页面根节点上:

const { windowHeight, windowWidth } = uni.getSystemInfoSync() const isCompact = windowHeight < 640 || windowWidth < 340

用640这个阈值是因为常见的最小可用高度在 667 左右(iPhone SE 一代),扣掉状态栏和底部安全区大约 100,剩下 567 左右,我把阈值放在 640 是留了一点余量。

7. 上线前我会逐条过的自测清单

这套页面我大概迭代了四五个版本,每次上线前都会照着下面这张表过一遍。表里的每一条都是被真实用户或者测试同学提过的问题,不是凭空想的。

检查项期望表现常见问题
状态栏避让标题不与胶囊重叠直接写死 44px,安卓上错位
输入框聚焦光标稳定不抖动改边框宽度导致重排
键盘弹起输入框不被遮挡安卓上键盘盖住表单
密码显隐点击即时切换用 type 切换需要重挂载
验证码倒计时页面返回后状态正确未清定时器,数字乱跳
协议未勾选有明确提示直接禁用按钮,用户不知道原因
按钮连点只发一次请求无冷却,重复提交
暗黑模式卡片有层级区分只靠阴影,暗色下看不见
小屏机型表单完整可见卡片超出屏幕,按钮被裁
低端机动画无肉眼可见卡顿setInterval 驱动动画

这份清单里,我认为最容易漏的是"验证码倒计时"和"暗黑模式"这两条。前者是因为开发时通常不会去测返回场景,后者是因为大多数人只在浅色模式下开发调试,等上线了才收到用户反馈。

关于这套页面的后续扩展,我目前想到两个方向:一是把输入框组件抽成一个不带任何业务语义的通用组件,放到组件库里,其他页面也能用;二是把整套颜色变量做成可配置的,接入主题切换能力,让运营可以在后台配一套品牌色,前端运行时注入。后者在小程序里有实现路径,用page级别的 CSS 变量注入,但要注意变量注入的时机必须在页面渲染前,否则会闪一下默认色。

最后分享一个我在实际操作中的体会:登录页这种"简单页面",真正的难点从来不在视觉,而在它要同时应付网络异常、用户误操作、机型差异、系统主题这四类不确定性。视觉稿给你的是理想状态,剩下的所有状态都得自己想清楚。我写这个系列的第三篇,也是因为前两篇把视觉做完之后,才发现真正花时间的是后面这些看不见的部分。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询