ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

uni-app微信小程序登录页实战:纯CSS视觉与低端机性能优化

uni-app微信小程序登录页实战:纯CSS视觉与低端机性能优化 1. 这套登录页到底解决什么问题做 uni-app 微信小程序这两年登录页面是我改动次数最多的一个页面。原因很实在它既是用户进来的第一屏又是整个鉴权链路的起点视觉上要好看逻辑上要稳还得兼容各种机型。前两篇我分别写过卡片式和插画式的登录页评论区反馈最多的两个问题一个是背景太素跟别人的小程序撞脸另一个是在安卓低端机上滑动卡得厉害。所以这一版 login-v3 我换了思路视觉上做减法但加层次交互上做加法但控成本最终目标就是——一套能直接复制进项目的登录页面视觉有记忆点逻辑能跑通低端机不掉帧。这篇东西适合谁看如果你正在用 uni-app 做微信小程序手头需要一个能落地的登录页又不想直接抄组件库那种一眼就看出出处的模板那这套思路应该对你有用。我会把布局、配色、动画、表单校验、倒计时、错误提示、真机适配全部拆开讲代码基本可以直接复制粘贴。涉及的关键词无非就是 uni-app、微信小程序、UI、登录页面这几样但我会尽量把每个参数为什么这么写讲清楚而不是甩一堆代码让你自己猜。需要先说明一点本文不是在讲某个万能模板而是在讲一套我在实际项目里反复调整过的做法。你在自己项目里用的时候根据品牌色和业务逻辑做替换就行骨架不用动。1.1 前两版留下的三个遗憾第一版用的是纯色背景加居中卡片开发十分钟就搞定了但上线之后视觉上确实没什么记忆点用户看一眼就忘。第二版加了插画和视差效果好看是好看了可插画资源打包进去之后主包体积涨了接近 400KB而且在一些千元安卓机上滑起来有明显的掉帧感首屏渲染时间从 600ms 左右拖到了 1.1s。第三个遗憾是逻辑层面的前两版的校验都写在按钮点击事件里用户输完手机号不移开焦点直接点登录手机上很容易出现最后一次输入没被读到的诡异现象本质上是blur和tap的事件顺序问题。这三个遗憾基本就是第三版的改造方向。资源体积要靠纯 CSS 实现视觉层次动画要控制在能用 transform 和 opacity 解决的范围内校验逻辑要拆成独立函数并且在输入过程中就实时跑不依赖提交那一刻的读取。提示登录页的资源体积是很多人忽视的点。小程序主包有 2MB 的限制登录页又是用户必经路径如果这里塞了大图后面业务页面就没什么余量了。1.2 第三版的取舍视觉做减法交互做加法视觉减法的意思是我把插画去掉了换成渐变底色加两个模糊光斑再加一层细网格纹理。这三个东西全部用 CSS 写不占任何打包体积还能跟着屏幕尺寸自适应。光斑用radial-gradient画网格用linear-gradient平铺配合filter: blur()做柔化整体观感比单纯色块厚实不少。交互加法的意思是输入框的聚焦态、错误态、禁用态全部做区分按钮有加载态和置灰态验证码按钮带倒计时并且做了节流错误提示用轻量 toast 加输入框下方红字双通道兼顾即时性和可读性。这些东西写起来代码量不小但都是纯逻辑不增加渲染压力。有一点要强调动画我全程只用transform和opacity两个属性。原因是这两个属性可以被合成器单独处理不触发重排重绘在低端机上表现最稳。你用top、left、width、height做动画在开发者工具里看不出问题一到真机就露馅。2. 动手前的准备工作与目录结构开工之前先理清楚工程结构不然写着写着文件就乱了。我习惯把登录页拆成三段页面本体、可复用的输入框组件、样式变量文件。虽然多建了两个文件但后面如果注册、找回密码页面也要用同样的输入框直接引组件就行不用复制粘贴一堆重复样式。2.1 工程配置与页面注册页面放在pages/login/login.vue输入框组件放components/ui-input/ui-input.vue全局样式变量放styles/variables.scss。在pages.json里注册页面的时候我一般会把导航栏关掉用自定义导航这样整屏的控制权都在自己手里。{ path: pages/login/login, style: { navigationStyle: custom, navigationBarTextStyle: black, backgroundColor: #f5f6fa } }navigationStyle设为custom之后页面顶部会一直顶到状态栏这时候你需要自己处理状态栏高度否则内容会被刘海屏的状态栏压住。状态栏高度通过uni.getSystemInfoSync().statusBarHeight获取这个值在不同机型上不一样iPhone 上是 44 或 47很多安卓机是 24 左右千万别写死。如果你用的是 scss记得在uni.scss里引入变量文件这样每个页面都能直接用不用重复 import。import /styles/variables.scss;注意navigationStyle: custom只在小程序和 App 端表现一致H5 端的行为略有差异。如果你的项目要同时发 H5建议在 H5 的样式分支里单独处理顶部间距。2.2 样式变量的统一管理我见过太多项目的登录页颜色是直接写死的十六进制值散落在十几个地方。等到品牌色一改得全局搜索替换还容易漏。所以从第三版开始我把所有颜色、圆角、字号、间距全部抽成变量。// styles/variables.scss $brand-primary: #4a6cf7; $brand-primary-light: #7b93ff; $brand-gradient: linear-gradient(135deg, #4a6cf7 0%, #7b93ff 100%); $text-main: #1a1a2e; $text-sub: #8a8fa3; $text-placeholder: #b8bccb; $bg-page: #f5f6fa; $bg-card: #ffffff; $border-color: #e8eaf0; $radius-card: 32rpx; $radius-input: 24rpx; $radius-btn: 48rpx; $space-sm: 16rpx; $space-md: 32rpx; $space-lg: 48rpx;这么做还有个隐藏好处设计稿换皮的时候只要改变量文件里的几个值整个页面风格就跟着变了不用动业务代码一行。另外$brand-gradient这种复合值也能抽后面按钮、图标背景都能复用。字号我一般不用变量因为小程序里的字号就那么几档抽出来反而影响阅读代码时的直观判断28rpx 就是 28rpx比$font-md一眼看过去更快。3. 视觉层的实现细节视觉这部分我拆成三块讲背景、卡片、输入框。背景决定了第一眼的观感卡片决定了页面的层次输入框决定了用户操作的舒适度。三块都是有坑的地方尤其背景那块很多人写出来的渐变在真机上是断层的。3.1 顶部渐变与光斑背景怎么做才不糊先说一个常见错误。很多人写渐变背景喜欢这样background: linear-gradient(to bottom, #4a6cf7, #f5f6fa 60%);在开发者工具里看是平滑的但部分安卓机在低色深屏幕下会出现明显的色带也就是俗称的断层。原因是渐变跨越的色彩空间太大8 位色深不够用。解决办法有两个一是把渐变的结束位置提前让颜色跨度变小二是叠一层极轻微的噪点纹理打散色带。我的做法是用两个模糊光斑来代替大面积渐变。具体就是在页面顶部放两个绝对定位的圆形给他们加径向渐变和模糊让它们自然晕开。view classbg-wrap view classblob blob-1/view view classblob blob-2/view view classgrid-mask/view /view.bg-wrap { position: fixed; inset: 0; overflow: hidden; background: #f5f6fa; z-index: -1; } .blob { position: absolute; border-radius: 50%; filter: blur(80rpx); opacity: 0.6; } .blob-1 { width: 520rpx; height: 520rpx; top: -180rpx; left: -140rpx; background: radial-gradient(circle, #7b93ff 0%, rgba(123, 147, 255, 0) 70%); } .blob-2 { width: 460rpx; height: 460rpx; top: 60rpx; right: -160rpx; background: radial-gradient(circle, #a5b8ff 0%, rgba(165, 184, 255, 0) 70%); }这里有个关键点filter: blur()在小程序里的性能开销跟模糊半径和元素面积正相关。上面这两个光斑面积不算小模糊半径 80rpx 已经是比较克制的值了。如果你加到 200rpx 以上低端机首屏渲染会明显变慢。实测下来两个光斑加起来的渲染时间在 30ms 以内属于可接受范围。网格纹理用平铺的线性渐变实现透明度压到 0.03 左右肉眼几乎看不出但能把大片纯色区域激活看起来不那么单薄。.grid-mask { position: absolute; inset: 0; background-image: linear-gradient(rgba(74, 108, 247, 0.06) 1rpx, transparent 1rpx), linear-gradient(90deg, rgba(74, 108, 247, 0.06) 1rpx, transparent 1rpx); background-size: 60rpx 60rpx; }提示光斑和网格都用position: fixed加z-index: -1这样页面滚动的时候背景不动视觉上更稳定。但要注意 iOS 上 fixed 定位在键盘弹出时的表现后面第 5 章会细说。3.2 卡片式表单的层次感卡片本身没什么难的难的是怎么让它浮起来而不是贴在背景上。我的做法是三层叠加一层白色底一层极淡的边框一层大范围低透明度的阴影。.form-card { margin: 0 $space-lg; padding: 60rpx 48rpx 56rpx; background: #ffffff; border-radius: $radius-card; border: 1rpx solid rgba(74, 108, 247, 0.08); box-shadow: 0 8rpx 24rpx rgba(74, 108, 247, 0.06), 0 24rpx 64rpx rgba(74, 108, 247, 0.1); }阴影这里我用了两层近的那层负责边缘的清晰感远的那层负责整体的悬浮感。如果你只用一层大阴影卡片边缘会显得发虚只用一层小阴影又浮不起来。注意阴影颜色不要用纯黑用品牌色的低透明度版本整体会更协调。圆角 32rpx 是个比较舒服的值。低于 24rpx 显得方正高于 40rpx 在小屏手机上会让内容区域变窄输入框跟着缩水。3.3 输入框的三种状态与光标颜色输入框是用户接触最多的元素状态设计必须清晰。我给它定义了四种状态默认、聚焦、错误、禁用。默认态是浅灰底加无边框聚焦态把底色变白并且加一圈品牌色的光晕边框错误态把边框换成红色并且下方出提示文字禁用态整体降透明度。.ui-input { display: flex; align-items: center; height: 96rpx; padding: 0 28rpx; background: #f7f8fc; border-radius: $radius-input; border: 1rpx solid transparent; transition: background-color 0.2s, border-color 0.2s, box-shadow 0.2s; } .ui-input.is-focus { background: #ffffff; border-color: rgba(74, 108, 247, 0.5); box-shadow: 0 0 0 6rpx rgba(74, 108, 247, 0.08); } .ui-input.is-error { background: #fff5f5; border-color: rgba(245, 87, 87, 0.5); }这里有个细节box-shadow我用了0 0 0 6rpx这种写法也就是只扩散不偏移相当于给元素加了一圈外发光。相比border-width变化这种写法不会引起布局抖动因为边框宽度没变。光标颜色在小程序里要单独设置微信小程序支持cursor-color属性但只在小程序端生效H5 端要用 CSS 的caret-color。input classinput-inner :valuemodelValue :passwordpassword :cursor-colorcursorColor placeholder-classinput-placeholder inputonInput focusonFocus bluronBlur /placeholder-class是个容易踩的坑。很多人在 style 里直接写.input-inner::placeholder在小程序里是不生效的必须通过placeholder-class指定一个类名然后在外层样式里写这个类。而placeholder-style可以写内联样式适合只改颜色不改字号的场景。4. 交互逻辑与数据流视觉搞定之后逻辑才是真正决定这个页面好不好用的地方。我把逻辑分成三块校验、按钮状态、验证码倒计时。三块都围绕一个原则——用户每做一步操作界面上必须有对应的即时反馈。4.1 表单校验规则的写法我不喜欢把校验写在提交函数里。第三版的做法是把每个字段的校验抽成一个纯函数输入过程中实时调用但错误提示延迟到失焦之后再展示。这样用户在输入的时候不会一直被红字骚扰但一离开输入框就能看到结果。const rules { phone: { test: (v) /^1[3-9]\d{9}$/.test(v), message: 请输入正确的手机号 }, code: { test: (v) /^\d{4,6}$/.test(v), message: 验证码为4到6位数字 }, agree: { test: (v) v true, message: 请先阅读并同意相关协议 } }; const validateField (key, value) { const rule rules[key]; if (!rule) return true; return rule.test(value); };注意我这里的正则只用了最基础的规则。有些团队喜欢写特别复杂的手机号正则把各种虚拟号段都排除掉这在实践中反而是坑。原因很简单号段是不断更新的规则写太死将来新号段放号用户就注册不了。基础校验只是拦截明显错误真正的有效性交给后端短信验证。关于同意协议这一项我的建议是不要做成必须勾选才能点按钮的形式用户体验很差。更好的是允许点击登录点击后如果没勾选弹一个轻提示并让勾选框闪一下。这种方式转化率明显更高。4.2 登录按钮状态机按钮我给它设计了三种状态用一个计算属性推导出来const btnState computed(() { if (loading.value) return loading; if (!canSubmit.value) return disabled; return normal; });其中canSubmit的判断要小心不能把同意协议算进去const canSubmit computed(() { return rules.phone.test(phone.value) rules.code.test(code.value); });按钮的视觉状态用类名切换加载态显示转圈图标并且禁用点击.btn-login { height: 96rpx; border-radius: $radius-btn; background: $brand-gradient; color: #fff; font-size: 32rpx; letter-spacing: 4rpx; transition: transform 0.15s, opacity 0.2s; } .btn-login:active { transform: scale(0.98); } .btn-login.is-disabled { background: #d4d8e5; opacity: 0.9; }transform: scale(0.98)这个按下反馈看起来不起眼但实测对点击率的感知提升挺明显用户会明确知道我按到了。注意用:active伪类在小程序里部分机型支持不完整稳妥一点可以用touchstart和touchend手动加类名。注意加载态一定要禁用重复点击。我见过线上事故就是网络慢的时候用户连点七八次后端收到一堆重复登录请求直接触发了风控。4.3 验证码倒计时与节流验证码按钮的逻辑看着简单坑其实最多。核心要求有三条点击后立即禁用防止重复请求倒计时期间显示剩余秒数页面切到后台再回来时倒计时不能乱。第一条用disabled或者自定义的if判断都行我用的是一个counting标志位。第二条就是常规的setInterval。第三条很多人忽略小程序切到后台之后定时器会被节流甚至暂停回来时秒数会不准。解决办法是记录一个结束时间戳每次 tick 的时候用当前时间和它做差。const endTime ref(0); let timer null; const startCountdown (seconds 60) { endTime.value Date.now() seconds * 1000; counting.value true; tick(); timer setInterval(tick, 1000); }; const tick () { const left Math.ceil((endTime.value - Date.now()) / 1000); if (left 0) { clearInterval(timer); timer null; counting.value false; countdown.value 0; return; } countdown.value left; };页面卸载的时候记得清理定时器不然会内存泄漏onUnmounted(() { if (timer) clearInterval(timer); });发送请求本身也要做一层防抖因为用户手快的话在disabled生效之前可能已经点了第二下。我的做法是加一个sending标志请求返回之前直接 return。5. 小程序端的适配与真机验证开发者工具里跑得好好的页面真机上出问题这是做小程序最常见的挫折。登录页尤其容易翻车因为它是用户进来看的第一屏各种刘海屏、折叠屏、大字号模式都会在这里暴露。这一章我讲三个最普遍的适配点。5.1 导航栏高度、胶囊按钮与安全区因为用了navigationStyle: custom页面顶部需要自己撑开。我的做法是在页面根节点上加一个 padding值等于状态栏高度。const statusBarHeight ref(0); onLoad(() { const info uni.getSystemInfoSync(); statusBarHeight.value info.statusBarHeight || 20; });view classpage :style{ paddingTop: statusBarHeight px }注意这里用的是 px 不是 rpx。状态栏高度是物理无关像素小程序返回的就是 px 单位的值如果你乘了 2 换算成 rpx 就错了。另外右上角有微信的胶囊按钮高度一般是 32px距离顶部有一个固定的偏移。如果你要在顶部放返回按钮或者标题必须和胶囊按钮垂直居中对齐否则看起来会错位。我的做法是拿胶囊按钮的位置信息来计算const menu uni.getMenuButtonBoundingClientRect(); const navBarHeight (menu.top - statusBarHeight.value) * 2 menu.height;这个公式算出来的是自定义导航栏的总高度。原理是胶囊按钮距离状态栏的距离上下应该对称所以顶部间距乘以 2 再加上胶囊高度就是导航栏的完整高度。底部安全区也别忘。iPhone 有底部横条如果登录按钮离底部太近会被横条挡住。用env(safe-area-inset-bottom)处理.page-footer { padding-bottom: calc(40rpx env(safe-area-inset-bottom)); }5.2 键盘弹起、rpx 与 1px 边框小程序里输入框聚焦时键盘弹起会把页面顶上去。input组件默认有adjust-position属性值为 true 时会自动上推页面。这在大部分情况下是好事但如果你的登录按钮是固定在底部的上推之后可能被键盘盖住。我的处理是把整个表单放在可滚动区域里adjust-position保持默认同时给底部留出足够的 padding。这样键盘弹起时用户能看到输入框滚动一下也能点到按钮。rpx 的换算要记牢规定屏幕宽度为 750rpx也就是说在 375px 宽的屏幕上1rpx 0.5px。所以在设计稿是 750 宽的情况下量到多少就写多少不用换算。但如果设计稿是 375 宽的就要把量到的数值乘以 2。1px 边框是经典问题。写border: 1px solid #eee在 2 倍屏上实际会渲染成 2 个物理像素看起来偏粗。小程序里的常规做法是用伪元素加transform: scaleY(0.5)来做细线。不过说实话登录页里的边框我一般用 1rpx 直接解决视觉效果已经够细了没必要为了极致去折腾伪元素方案。5.3 真机调试的检查清单每次改完登录页我都会按照这份清单在真机上过一遍。列出来给你参考检查项关注点常见问题状态栏内容不被刘海遮挡自定义导航没加 padding胶囊避让顶部元素不与胶囊重叠标题位置偏左或偏右键盘弹起输入框可见按钮可点底部按钮被键盘盖住输入法切换中文输入法下拼音不上屏用 input 而非 change 实时校验会误判错误提示提示文字不被键盘遮挡提示位置写死在输入框下方固定距离弱网按钮加载态正确显示请求没有超时处理一直转圈大字号模式布局不错乱固定高度导致文字被裁切深色模式如果支持配色对比度足够白底黑字硬编码关于中文输入法那条我要多说一句。用户用拼音输入时input事件会在拼音阶段就触发这时候拿到的值可能是 zhangsan 这种中间态。如果你的实时校验逻辑比较严格会在用户还没输完的时候就把输入框标红体验很糟。解决办法是配合compositionstart和compositionend判断是否在组词状态组词期间不触发校验。6. 常见问题与排查技巧实录前面讲的是怎么做这一章讲做完了之后会碰到什么。我把这两年在这个页面上踩过的坑整理了一下按出现频率排序。6.1 问题速查表现象可能原因处理方式输入框里文字被光标挡住内边距不足左右 padding 至少 24rpx焦点切换时页面跳动错误提示占位变化提示区域预留固定高度安卓机背景模糊卡顿blur 半径过大降到 80rpx 以内或改用多层径向渐变iOS 上背景 fixed 抖动键盘弹起触发重排背景改用 absolute 加页面高度 100vh倒计时秒数不准后台定时器被节流用时间戳差值计算剩余时间按钮点击无反应被透明遮罩挡住检查 z-index 和 pointer-eventsplaceholder 样式不生效用了 ::placeholder改用 placeholder-class提交后按钮一直转圈请求无超时兜底加 finally 复位 loading倒计时那条我再展开说一点。小程序切后台之后setInterval在 iOS 上会被限制到 1 秒以上甚至暂停安卓上相对宽松。所以不管你用哪种方式实现倒计时都别用每次 tick 减一的写法一定要用目标时间戳做基准。这个习惯养成之后不只是验证码任何和时间相关的逻辑都会稳很多。6.2 几个只有踩过才知道的细节第一个细节输入框的类型属性。手机号输入一定要设typenumber验证码如果只有数字也设typenumber。但要注意typenumber在部分安卓机上键盘的完成按钮行为不一致有的会直接收起键盘有的会切到下一个输入框。如果你希望用户输完手机号直接跳到验证码框可以在confirm里手动focus下一个输入框比依赖系统行为可靠。第二个细节错误提示的位置。我最初把提示文字放在输入框正下方结果输入框一多下面的元素就往下挤整个卡片高度变化视觉上很跳。后来改成在输入框内部右侧显示一个小图标提示信息用顶部的轻提示条展示。这样布局高度恒定不会被撑开。第三个细节关于登录接口的错误处理。后端返回的错误码要区分对待手机号格式错误、验证码错误这类是用户可修复的弹提示让用户改token 过期、账号被锁这类需要跳转或者联系客服的要给出明确的下一步指引。不要所有错误都弹一句登录失败那样用户完全不知道该怎么办。第四个细节也是我觉得最值得说的一个登录成功之后的跳转。很多人直接uni.switchTab或者uni.reLaunch到首页但没处理好页面栈。如果你的登录页是从某个业务页面跳转过来的登录完之后把用户扔到首页用户就得重新走一遍之前的路径体验很割裂。更合理的做法是在跳转登录页的时候带上redirect参数登录成功后优先回到这个地址没有参数再去首页。const redirect options.redirect ? decodeURIComponent(options.redirect) : ; const goAfterLogin () { if (redirect) { uni.reLaunch({ url: redirect }); } else { uni.switchTab({ url: /pages/index/index }); } };这里用reLaunch而不是navigateTo是为了清空页面栈。登录页不应该留在栈里否则用户从首页返回还能回到登录页逻辑上说不通。最后一个心得关于这套页面的复用。我在ui-input组件上暴露了v-model、type、password、error、disabled这几个属性其余全部内部处理。这样在注册页、找回密码页、实名认证页都能直接复用不用重复写样式。组件化这件事在登录页上收益特别高因为表单元素高度重复抽出来之后维护成本能降一大截。你如果现在手上正好有多个表单页面建议先花半天把输入框组件抽出来后面能省下好几天的调整时间。
返回列表