ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

uni-app 微信小程序登录页第三版:三态输入框与真机适配

uni-app 微信小程序登录页第三版:三态输入框与真机适配 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 classlogin-page view classglow / view classbrand image classlogo src/static/logo.png modeaspectFit / text classtitle欢迎回来/text text classsubtitle登录后继续你的工作台/text /view view classform-card login-field v-modelphone typephone placeholder请输入手机号 / login-field v-modelcode typecode placeholder请输入验证码 / login-button :loadingloading :disabled!canSubmit taphandleSubmit 登 录 /login-button /view agree-bar v-modelagreed / /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 classfield :classfieldClass view classfield__icon text classiconfont{{ iconMap[type] }}/text /view input classfield__input :valuemodelValue :typeinputType :passwordtype password !visible :maxlengthmaxlength :placeholderplaceholder placeholder-classfield__placeholder :adjust-positionfalse inputonInput focusfocused true blurfocused false / view v-ifshowClear modelValue classfield__clear taphandleClear text classiconfont✕/text /view /view /templateadjust-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组件有两个控制密码显示的属性typepassword和:passwordtrue。在小程序端推荐用password布尔属性配合typetext因为typepassword在部分基础库版本下切换显隐时需要重新挂载节点才能生效表现为点一下眼睛图标没反应再点一下才变。用: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 主按钮的禁用态、加载态与防重复点击主按钮最核心的功能不是好看是防止用户连点。网络慢的时候用户会本能地连点五下如果没做拦截就会发出五个登录请求轻则重复发送验证码重则账号被风控。我的做法是三保险disabled属性绑到计算属性上手机号或验证码不合法时直接禁用请求发出后把loading置为true按钮显示加载状态并拒绝点击在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 classagree-bar taptoggle view classagree-bar__box :class{ is-checked: modelValue } text v-ifmodelValue classiconfont✓/text /view text classagree-bar__text 我已阅读并同意 /text text classagree-bar__link tap.stopopenAgreement(user)《用户协议》/text text classagree-bar__text和/text text classagree-bar__link tap.stopopenAgreement(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-positionfalse关掉系统自动上推。关掉之后我需要自己控制布局。思路是监听键盘高度变化如果输入框的底部位置低于屏幕高度 - 键盘高度就把表单容器整体上移相应的距离。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 变量注入但要注意变量注入的时机必须在页面渲染前否则会闪一下默认色。最后分享一个我在实际操作中的体会登录页这种简单页面真正的难点从来不在视觉而在它要同时应付网络异常、用户误操作、机型差异、系统主题这四类不确定性。视觉稿给你的是理想状态剩下的所有状态都得自己想清楚。我写这个系列的第三篇也是因为前两篇把视觉做完之后才发现真正花时间的是后面这些看不见的部分。
返回列表