ARTICLE DETAIL

资讯详情

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

uni-app微信小程序登录页:轻玻璃拟态+光斑背景纯CSS实战

uni-app微信小程序登录页:轻玻璃拟态+光斑背景纯CSS实战 1. 先想明白这个登录页到底要解决什么问题做 uni-app 微信小程序的这几年我经手的项目里登录页面几乎是被改得最频繁的一个。产品经理的审美一周一变运营隔三差五想加个活动入口老板刷到别家小程序觉得好看又让换配色。所以我把这几年沉淀下来的登录页方案整理成了一个系列前面几篇分别聊过渐变卡片风、插画顶图风和极简暗色风这篇是第四篇主角是轻玻璃拟态加光斑背景这一套视觉语言。它适合预算不多、没有专职设计师、但又希望登录页别太原生丑的中小团队也适合刚上手 uni-app 微信小程序、想找个能直接抄作业的模板的新人。先把定位说清楚这一篇不讲登录接口怎么对接这种后端话题只聚焦一件事——在 uni-app 里用纯 CSS 把登录页的 UI 做出质感同时保证它在真机上不崩、不卡、不糊。这四个字看着简单真做起来坑不少。我见过太多页面在开发者工具里美得像概念稿一上真机Android 上光斑糊成一块脏色iOS 上输入框被键盘顶飞老机型上直接白屏。所以这篇的思路是先讲结构分层怎么定再讲每一个视觉元素的实现细节和降级方案然后给完整代码最后把我在真机上踩过的坑整理成一张速查表。你如果只想要代码直接跳到第 3 节如果想让自己的页面在别人手机上不翻车第 2 节和第 4 节建议认真看。1.1 从能登录到愿意登录的三层目标我把登录页的职责拆成三层这个拆法对后面所有技术决策都有指导意义。第一层是功能层手机号能不能输进去、验证码能不能收到、点登录有没有反应、异常有没有提示。这一层做不好页面再好看都是废的。第二层是信任层用户第一次打开你的小程序看到的第一个像样的界面很可能就是登录页。它要传达的是这个产品是有人在认真做的。一个连输入框都歪的登录页用户充值的意愿会直接打折这话不是我说的是无数 A/B 测试的结论。第三层是效率层微信生态里用户最舒服的登录方式是一键授权其次是手机号验证码。如果用户在你的页面上要花二十秒才能进去那这个页面的视觉设计就是失败的。好看必须让位于快。第四篇这套方案在这三层上的取舍是功能层做到极致稳信任层用有限的视觉手段拉满质感效率层则保留一键登录按钮并放在手机号输入框上方。为什么不放在下方因为拇指的舒适区在屏幕下半部分但用户的第一动作是决定用哪种方式登录这个决策点应该更靠近视线焦点也就是屏幕中上部。这是我做完几轮用户测试之后改的原来放下面一键登录的点击率大概低了七八个百分点。1.2 技术选型为什么不用现成组件库的登录模板每次我建议团队自写登录页样式都会有人问uView、uni-ui 不都有现成的登录模板吗为什么还要手写理由有三个而且都是实打实的成本。第一是覆盖成本高于重写成本。组件库的登录模板通常是一个 input 加一个 button 的极简结构你想改成光斑背景加玻璃卡片需要覆盖的样式类可能有三四十个还要跟组件库的默认主题变量打架。改到最后你会发现写一个新页面的时间比覆盖旧页面少一半。第二是体积。登录页往往是小程序的首屏用户打开小程序第一眼看到的就是它。如果为了这一个页面引入整个组件库主包体积上去首屏加载时间就上去。我实测过一个中等规模的组件库仅仅引入 Form 和 Button 两个组件的样式主包就多了一百多 KB。登录页这种结构简单的页面纯 CSS 写出来样式部分压缩后通常不到 10KB。第三是可控性。登录页是最需要像素级控制的页面之一因为它的元素少、每个元素都被放大审视。组件库给你的是一套通用解但通用解往往意味着妥协。手写的样式你想让边框从 1rpx 改成 2rpx 就是改一个数字不用担心影响到别的页面。当然这不是说组件库没用。列表页、表单页、弹窗这些复杂的组合场景组件库能省下大量时间。我的原则是结构简单的页面手写结构复杂的页面用库。登录页恰好属于前者。1.3 这套方案的页面结构分层我把整个登录页拆成四层从下往上依次是背景层负责氛围包含底色渐变和三个光斑。这一层完全不接收点击事件用pointer-events: none屏蔽掉避免它拦截表单的触摸。这个细节很多人会漏尤其是在光斑用了绝对定位、覆盖范围很大的时候。品牌层logo、主标题、副标题。它的作用是在用户还没开始操作时先建立印象高度通常占屏幕的 25% 左右太多了会挤压表单空间。表单层一键登录按钮、分割线、手机号输入框、验证码输入框加获取按钮、登录按钮。这是整个页面的核心所有的交互都在这一层。辅助层用户协议勾选、其他登录方式、客服入口。这一层字号最小、颜色最淡但不能省因为协议勾选是合规硬要求。分层的意义在于你可以单独调换其中任意一层而不影响其他层。比如把光斑背景换成插画只需要替换背景层的代码表单层一行都不用动。我在实际项目里就是这么干的同一套表单层代码配合不同的背景层能快速产出好几种风格的登录页这也是我把方案做成系列的原因。2. 视觉骨架怎么搭尺寸、颜色、层次结构定了接下来是把骨架立起来。这一节全部是具体参数和实现手法每一个数字我都会解释为什么是这个值。2.1 尺寸单位与屏幕适配从 750rpx 说起uni-app 里最常用的尺寸单位是rpx规则很简单屏幕宽度恒等于 750rpx。也就是说在 375px 宽的设备上1rpx 等于 0.5px在 414px 宽的设备上1rpx 约等于 0.552px。这个机制的好处是设计稿可以直接按 750 宽度出标注多少就写多少。但 rpx 有个坑屏幕越宽rpx 换算出的物理尺寸越大。在手机上这个自适应是合理的但到了平板或者折叠屏展开态一个按 750rpx 设计的登录卡片可能会被拉到 900px 宽按钮变得又扁又长非常难看。解决办法是给内容容器加一个最大宽度限制。我的做法是.form-card { width: 630rpx; max-width: 420px; margin: 0 auto; }这里 630rpx 对应设计稿上左右各留 60rpx 的边距视觉上比较舒服不会太顶边也不会太局促。max-width: 420px是兜底保证在任何超宽屏上卡片都不会超过这个物理宽度。注意单位是 px 不是 rpx因为我们要的是物理尺寸的绝对上限。垂直方向的间距我建议统一用 rpx但慎用vh。原因是在微信小程序里100vh包含了状态栏和底部安全区键盘弹起时 vh 的表现也不一致。与其跟它斗智斗勇不如老老实实用 rpx 加百分比。提示设计稿如果不是 750 宽度比如是 375所有标注值要乘以 2 再写进 rpx。这个换算错误导致的对不齐问题我见过太多次了。2.2 光斑背景不用 filter 也能做出软边玻璃拟态的灵魂是背景里那几团模糊的光斑。网上的教程大多教你用filter: blur(80px)但这是最不稳的做法。我在多个机型上实测过微信小程序的渲染层对 CSSfilter的支持是有差异的。iOS 相对好一些部分 Android 机型上filter: blur()要么不生效要么直接导致整个元素渲染异常、出现方块撕裂。更关键的是模糊滤镜是 GPU 密集操作在大尺寸元素上很容易引起滚动掉帧。我的替代方案是用径向渐变直接画出软边圆零滤镜、零 GPU 压力.blob { position: absolute; border-radius: 50%; pointer-events: none; will-change: transform; } .blob-1 { width: 520rpx; height: 520rpx; top: -160rpx; left: -180rpx; background: radial-gradient( circle at 50% 50%, rgba(99, 102, 241, 0.55) 0%, rgba(99, 102, 241, 0.28) 35%, rgba(99, 102, 241, 0) 70% ); } .blob-2 { width: 460rpx; height: 460rpx; top: 240rpx; right: -200rpx; background: radial-gradient( circle at 50% 50%, rgba(236, 72, 153, 0.45) 0%, rgba(236, 72, 153, 0.2) 40%, rgba(236, 72, 153, 0) 72% ); } .blob-3 { width: 600rpx; height: 600rpx; bottom: -260rpx; left: -120rpx; background: radial-gradient( circle at 50% 50%, rgba(56, 189, 248, 0.4) 0%, rgba(56, 189, 248, 0) 68% ); }关键在于渐变里那个0%的透明度落点。如果写rgba(x, x, x, 0) 50%边缘会太硬看起来像个实心圆写70%左右过渡最自然。三个光斑的透明度和尺寸也不应该完全一样0.55 / 0.45 / 0.4这种递减能形成主次关系画面才有呼吸感。还有一个性能细节光斑是静态的不需要动效的话千万别给它加动画。我早期版本给光斑加了一个 20 秒的缓慢位移在低端 Android 上直接导致首屏掉到 30 帧以下。静态渐变已经足够好看动态光斑属于典型的设计师觉得好工程师觉得亏。2.3 玻璃卡片的降级方案玻璃卡片的核心属性是backdrop-filter: blur()它能让卡片背后的内容模糊化形成磨砂质感。但这个东西在小程序里的支持情况说实话不建议当作核心依赖。我的做法是两步走先给一个不依赖任何新特性的基础样式再用supports做渐进增强。基础样式用半透明白底加细边框.form-card { position: relative; z-index: 2; width: 630rpx; max-width: 420px; margin: 0 auto; padding: 56rpx 40rpx 48rpx; border-radius: 40rpx; background: rgba(255, 255, 255, 0.72); border: 1rpx solid rgba(255, 255, 255, 0.9); box-shadow: 0 20rpx 60rpx rgba(31, 41, 55, 0.1), inset 0 1rpx 0 rgba(255, 255, 255, 0.8); }这三个属性的组合很关键。半透明白底负责提供玻璃的厚度1rpx的白色描边是玻璃边缘的高光inset的内阴影模拟的是玻璃顶面的反光。这三样凑在一起即使没有模糊滤镜视觉上也有七成玻璃的感觉了。如果要做增强可以在支持的环境下追加supports (backdrop-filter: blur(20px)) { .form-card { background: rgba(255, 255, 255, 0.55); backdrop-filter: blur(20px); } }注意这里把底色的透明度从 0.72 降到了 0.55。原因是有模糊之后背景的光斑本来就会被柔化底色太实反而会把光斑盖住玻璃感就没了。这个数值我调了大概五六版0.55 是个比较平衡的点。注意即使supports判断通过也不代表真机上一定渲染正确。上线前务必在至少一台 Android 中端机和一台 iOS 设备上各看一眼别只看开发者工具。2.4 输入框与按钮的细节处理这部分是决定精致度的地方。同一个布局细节处理得好和不好观感差一个档次。输入框我用软填充的方案而不是传统的下划线。下划线风确实干净但在玻璃卡片里下划线会和卡片的边缘高光打架视觉上很乱。软填充就是一个浅灰底加圆角.field { display: flex; align-items: center; height: 96rpx; padding: 0 24rpx; border-radius: 24rpx; background: rgba(243, 244, 246, 0.9); border: 1rpx solid rgba(229, 231, 235, 0.8); transition: border-color 0.2s, background-color 0.2s; } .field.is-focus { border-color: rgba(99, 102, 241, 0.6); background: rgba(255, 255, 255, 0.95); }高度 96rpx 对应 48px这是移动端输入框比较舒服的触控高度符合 44px 最小可点击区域的经验值。圆角 24rpx 和卡片的 40rpx 形成层级差外大内小视觉上更协调。聚焦态必须做。用户点了输入框如果没有视觉反馈他会怀疑是不是没点上。我用的是边框变主题色加底色提亮这两样同时变化反馈足够明显又不会太跳。按钮的渐变方向有讲究。从左到右的横向渐变在小按钮上效果一般我改成135deg的斜向渐变色彩过渡更有张力.btn-primary { height: 96rpx; border-radius: 48rpx; font-size: 32rpx; font-weight: 600; color: #ffffff; background: linear-gradient(135deg, #6366f1 0%, #8b5cf6 50%, #d946ef 100%); box-shadow: 0 12rpx 32rpx rgba(99, 102, 241, 0.32); border: none; } .btn-primary::after { border: none; } .btn-primary.is-disabled { background: linear-gradient(135deg, #c7d2fe 0%, #ddd6fe 100%); box-shadow: none; color: rgba(255, 255, 255, 0.9); }::after那一句是必须的。微信小程序的button组件自带一个伪元素边框不干掉的话你的圆角按钮外面会多一圈方框。这个坑几乎每个新手都要踩一次。阴影用主题色而非黑色透明度控制在 0.32 左右。黑色阴影会让按钮显得脏同色系阴影才是现代感的来源。这个技巧在任何 UI 上都适用可以记一下。3. 把代码敲出来完整实操过程前面讲了原理这一节给可以直接跑的完整代码。我用的是 uni-app 的 Vue3 组合式 API 写法script setup语法糖。3.1 页面模板结构template view classlogin-page view classblob blob-1/view view classblob blob-2/view view classblob blob-3/view view classbrand view classlogo-wrap image classlogo src/static/logo.png modeaspectFit / /view text classbrand-title欢迎回来/text text classbrand-sub登录后继续你的旅程/text /view view classform-card button classbtn-primary btn-wx open-typegetPhoneNumber getphonenumberonGetPhoneNumber 微信一键登录 /button view classdivider view classdivider-line/view text classdivider-text或使用手机号登录/text view classdivider-line/view /view view :class[field, { is-focus: focusField phone }] text classfield-icon86/text input classfield-input typenumber maxlength11 placeholder请输入手机号 placeholder-classph :valuephone inputonPhoneInput focusfocusField phone blurfocusField / view v-ifphone classfield-clear tapphone ×/view /view view :class[field, { is-focus: focusField code }] input classfield-input typenumber maxlength6 placeholder请输入验证码 placeholder-classph :valuecode inputonCodeInput focusfocusField code blurfocusField / view :class[code-btn, { is-off: counting 0 || !phoneValid }] tapsendCode {{ counting 0 ? counting s : 获取验证码 }} /view /view button classbtn-primary btn-login :class{ is-disabled: !canSubmit } :disabled!canSubmit taphandleLogin 登录 /button view classagree tapagreed !agreed view :class[checkbox, { is-checked: agreed }]/view text classagree-text 我已阅读并同意 text classlink tap.stopopenDoc(user)《用户协议》/text 和 text classlink tap.stopopenDoc(privacy)《隐私政策》/text /text /view /view /view /template模板里有几个细节值得单独说。一是清除按钮。手机号输入框右侧那个×只有在有内容的时候才显示。移动端打字容易出错尤其是数字键盘给一个一键清空的入口体验提升非常明显。这个按钮的点击区域我用padding撑到了 60rpx 见方虽然图标本身很小但可点区域足够大不会点不中。二是分割线用的是两侧线条加中间文字的结构而不是单纯的一条线加个文字。这样文字不会压在线上视觉更整洁。三是协议勾选的点击区域。我把整个agree容器都绑定了点击事件而不是只绑那个小方块。小方块的物理尺寸只有 32rpx也就是 16px 左右单独点它非常费劲。整个容器绑事件之后用户点到文字上也能勾选体验好很多。3.2 样式实现从背景到卡片的完整 CSS完整的样式我按层次拆开贴。先看页面容器和背景.login-page { position: relative; min-height: 100vh; padding: 0 60rpx; box-sizing: border-box; overflow: hidden; background: linear-gradient(170deg, #eef2ff 0%, #faf5ff 45%, #f0f9ff 100%); } .brand { position: relative; z-index: 2; padding: 160rpx 0 72rpx; display: flex; flex-direction: column; align-items: center; } .logo-wrap { width: 132rpx; height: 132rpx; border-radius: 36rpx; background: rgba(255, 255, 255, 0.85); display: flex; align-items: center; justify-content: center; box-shadow: 0 16rpx 40rpx rgba(99, 102, 241, 0.16); } .logo { width: 80rpx; height: 80rpx; } .brand-title { margin-top: 36rpx; font-size: 48rpx; font-weight: 700; color: #1f2937; letter-spacing: 1rpx; } .brand-sub { margin-top: 14rpx; font-size: 26rpx; color: #6b7280; letter-spacing: 0.5rpx; }这里padding: 160rpx 0 72rpx的上边距给得比较足。为什么要 160rpx因为小程序右上角有胶囊按钮如果 logo 太靠上会和胶囊在视觉上打架显得拥挤。160rpx 大约是 80px在大多数机型上能把品牌区推到胶囊下方的安全位置。这个数值你不用死记在真机上看一眼觉得不挤就行。再看表单内部的元素.field .field { margin-top: 24rpx; } .field-input { flex: 1; height: 100%; font-size: 30rpx; color: #1f2937; padding-left: 16rpx; background: transparent; } .field-icon { font-size: 28rpx; color: #4b5563; font-weight: 600; padding-right: 16rpx; border-right: 1rpx solid rgba(209, 213, 219, 0.9); line-height: 1; } .field-clear { width: 60rpx; height: 60rpx; line-height: 56rpx; text-align: center; font-size: 36rpx; color: #9ca3af; border-radius: 50%; } .code-btn { padding: 12rpx 24rpx; font-size: 26rpx; color: #6366f1; font-weight: 600; border-radius: 16rpx; background: rgba(99, 102, 241, 0.1); } .code-btn.is-off { color: #9ca3af; background: rgba(156, 163, 175, 0.12); }注意.field .field这个相邻兄弟选择器。用它来做间距比给每个元素单独写 margin 更干净也不用担心第一个元素多出上边距。但不要用gap这是我要重点提醒的坑。微信小程序在部分基础库版本上对 flex 的gap属性支持不完整尤其是一些 Android 机型的低版本基础库gap会被直接忽略导致的后果是所有输入框挤在一起非常难看。用 margin 是零风险的方案。分割线和协议区.divider { display: flex; align-items: center; margin: 40rpx 0 32rpx; } .divider-line { flex: 1; height: 1rpx; background: rgba(209, 213, 219, 0.8); } .divider-text { padding: 0 20rpx; font-size: 24rpx; color: #9ca3af; } .agree { display: flex; align-items: flex-start; margin-top: 36rpx; padding: 8rpx 0; } .checkbox { width: 32rpx; height: 32rpx; border-radius: 50%; border: 2rpx solid #d1d5db; margin-right: 14rpx; margin-top: 4rpx; flex-shrink: 0; position: relative; transition: all 0.2s; } .checkbox.is-checked { background: #6366f1; border-color: #6366f1; } .checkbox.is-checked::after { content: ; position: absolute; left: 9rpx; top: 4rpx; width: 8rpx; height: 14rpx; border-right: 3rpx solid #ffffff; border-bottom: 3rpx solid #ffffff; transform: rotate(45deg); }那个勾选的对勾是用伪元素拼出来的两个边框加 45 度旋转。这个技巧不用引图标字体也不用图片体积为零改颜色也方便。left和top的偏移值需要根据你实际设置的勾选框尺寸微调我给的是 32rpx 尺寸下的经验值。3.3 交互逻辑手机号校验与验证码倒计时逻辑部分用script setup写script setup import { ref, computed, onUnmounted } from vue const phone ref() const code ref() const agreed ref(false) const counting ref(0) const focusField ref() let timer null const phoneValid computed(() /^1[3-9]\d{9}$/.test(phone.value)) const canSubmit computed( () phoneValid.value code.value.length 6 agreed.value ) function onPhoneInput(e) { phone.value String(e.detail.value).replace(/\D/g, ).slice(0, 11) } function onCodeInput(e) { code.value String(e.detail.value).replace(/\D/g, ).slice(0, 6) } function sendCode() { if (counting.value 0) return if (!phoneValid.value) { uni.showToast({ title: 手机号格式不正确, icon: none }) return } counting.value 60 timer setInterval(() { counting.value - 1 if (counting.value 0) { clearInterval(timer) timer null } }, 1000) // 这里调用后端的发送验证码接口 // uni.request({ url: /api/sms/send, method: POST, data: { phone: phone.value } }) } async function handleLogin() { if (!agreed.value) { uni.showToast({ title: 请先阅读并同意相关协议, icon: none }) return } if (!canSubmit.value) return uni.showLoading({ title: 登录中, mask: true }) try { const res await uni.request({ url: /api/auth/login, method: POST, data: { phone: phone.value, code: code.value } }) uni.setStorageSync(token, res.data.token) uni.reLaunch({ url: /pages/index/index }) } catch (err) { uni.showToast({ title: 登录失败请重试, icon: none }) } finally { uni.hideLoading() } } function onGetPhoneNumber(e) { if (!e.detail.code) { uni.showToast({ title: 你取消了授权, icon: none }) return } // 把 e.detail.code 交给后端换取手机号再由后端下发登录态 // uni.request({ url: /api/auth/phone, method: POST, data: { code: e.detail.code } }) } function openDoc(type) { uni.navigateTo({ url: /pages/doc/index?type${type} }) } onUnmounted(() { if (timer) clearInterval(timer) timer null }) /script有几个点必须解释清楚。倒计时必须在onUnmounted里清掉。这个是最容易被忽略的。用户点了获取验证码然后在 60 秒内点了返回退出页面如果定时器没清它会继续跑而它引用的counting已经是个没人管的变量了。轻则内存泄漏重则在小程序里报setData 到已销毁页面的警告。养成习惯凡是setInterval旁边就要有clearInterval。输入过滤要在输入时做不要只在校验时做。我用replace(/\D/g, )在输入的瞬间就把非数字字符干掉用户根本打不进去字母。这比事后弹一个请输入正确的手机号友好太多。同时用slice限制了长度双保险。button的disabled和自定义的is-disabled类要同时用。只用类名按钮虽然看起来是灰的但还能点只用disabled属性微信的默认禁用样式会和你的设计冲突。两个一起用视觉和交互才一致。微信一键登录的open-type属性。注意getPhoneNumber事件回调里拿到的是e.detail.code这个 code 是要交给你的后端去换手机号的。千万不要在前端直接拿它去做任何解密操作那些密钥必须留在服务端。这是安全底线也是微信官方的要求。3.4 键盘弹起与布局调整登录页最经典的翻车场景就是键盘弹起把布局顶乱。微信小程序的input默认有adjust-position属性值为true时页面会自动上推。这个默认行为在长表单里是必要的但在登录页这种内容不多的场景里自动上推会让整个页面突然跳一下观感很差。我的做法是把输入框的自动上推关掉改成手动控制input classfield-input :adjust-positionfalse focusonFocus bluronBlur /然后在逻辑里监听键盘高度const kbHeight ref(0) function onFocus() { uni.onKeyboardHeightChange((res) { kbHeight.value res.height }) } function onBlur() { kbHeight.value 0 }拿到高度之后用一个包裹容器的transform: translateY()来平移页面。平移量的计算方式是键盘高度减去输入框底部到屏幕底部的距离如果结果是负数就取 0。这样能保证输入框刚好被顶到键盘上方而不是无脑平移一大堆。const offset computed(() { const need kbHeight.value - 120 return need 0 ? -need : 0 })那个 120 是个经验缓冲值给输入框上方留一点呼吸空间。你可以根据自己的布局调我一般调到输入框上沿离键盘顶部大约 40px 就停了。注意uni.onKeyboardHeightChange是全局监听页面销毁时记得用uni.offKeyboardHeightChange取消否则会在多个页面之间互相干扰。4. 真机踩坑与排查实录这一节是这篇最有价值的部分。下面每一条都是我或者同事在真机上真实遇到的问题不是从文档里抄的。4.1 样式类问题速查先上一张表方便你对着排查。现象可能原因处理方式按钮圆角外多一个方框小程序 button 自带::after边框加button::after { border: none; }输入框之间完全贴在一起使用了 flexgap低版本基础库不支持改用相邻兄弟选择器的 margin光斑边缘生硬像实心圆渐变透明落点太小把透明色停靠点从 50% 调到 68%~72%Android 上背景一块块发灰使用了filter: blur()换成径向渐变去掉 filter文字在不同机型上粗细不一用了font-weight: 500这类中间值只使用 400 和 700中间值在部分系统上会被舍入1rpx 边框在部分机型上消失1rpx 换算后小于 1 物理像素关键边框改成 2rpx或改用transform: scaleY(0.5)这里重点说两条。第一是font-weight的中间值问题。很多人喜欢写font-weight: 500或者600在 iOS 上看起来是半粗效果很好。但在部分 Android 系统的默认字体里没有对应的字重浏览器会把它舍入到最近的可用值结果可能是 400 也可能是 700同一份代码在不同手机上粗细完全不同排版看着就乱了。稳妥的做法是只用 400 和 700 两个值需要强调就用 700正文字用 400。这个建议听起来有点土但它能帮你省掉一堆为什么用户反馈字体不对的客服工单。第二是 1rpx 边框消失。在 375px 宽的设备上1rpx 等于 0.5 个物理像素。很多渲染引擎会把小于 1 物理像素的线做抗锯齿处理结果就是这条线变得极淡甚至在某些屏幕上完全看不见。如果你需要一条细但一定看得见的分割线可以用两条技巧要么直接上 2rpx要么用transform: scaleY(0.5)把一条 2rpx 的线压扁。后者能保证在任何倍率的屏幕上都是一致的半像素观感。4.2 交互类问题问题一输入框聚焦时页面整体错位。前面 3.4 节讲的手动控制就是解决这个的。还有一个补充技巧给页面容器设overflow: hidden之后如果同时用了position: fixed的底部元素键盘弹起时fixed元素不会跟着平移会悬浮在键盘上面看起来像贴在空中。解决办法是把fixed元素也纳入到同一个 transform 容器里或者干脆不用 fixed改成 flex 布局的底部对齐。问题二获取验证码按钮被连点多次。虽然逻辑里有if (counting 0) return挡着但在极短的时间窗口内比如用户用连点器还是可能触发两次请求。更稳的做法是在发出请求的瞬间就把counting设成 60然后再发请求请求失败的话再把counting重置为 0。这样即使请求还没回来按钮也已经是禁用状态了。问题三一键登录的授权弹窗被用户拒绝后再点没反应。微信的getPhoneNumber在某些情况下会返回失败回调里e.detail.code是空的。这时候要给用户一个明确的提示并且让他可以再次尝试。我见过有的实现是把按钮disabled掉了用户拒绝了一次之后按钮就永久灰了只能杀掉小程序重进这个体验非常糟糕。正确做法是无论成功失败按钮都保持可点只是失败时弹个 toast 说明原因。问题四协议勾选框在小屏手机上被挤出可视区。小屏设备比如 5.4 英寸的机型上如果品牌区高度给得太大表单卡片会被推到屏幕下方协议区就跑到折叠线以下了。解决办法是给品牌区的 padding 用一个较小的下限值或者在onLoad里读取uni.getSystemInfoSync().screenHeight屏幕高度小于某个阈值时动态缩小品牌区的高度。我一般设的阈值是 700px低于这个值就把顶部 padding 从 160rpx 减到 100rpx。4.3 性能与体积优化登录页是首屏它的性能直接影响用户对小程序的第一印象。有几个优化动作是投入产出比很高的。图片资源全部走image组件加本地静态图不要用 CSS 背景图。logo 这种小图用image组件配合modeaspectFit加载和缓存都由小程序统一管理比 CSS 背景图稳定。而且image组件支持懒加载配置虽然登录页的图通常在首屏但统一管理总归更好。减少不必要的层级嵌套。我见过有的登录页嵌套了七八层 view每一层都有自己的样式。这在小程序里会明显增加渲染树的深度影响首屏的布局计算时间。我的原则是能用一层搞定的绝不用两层装饰性元素能合并就合并。避免在首屏使用大量阴影。阴影是便宜的精致但它有成本。每一个box-shadow都会触发额外的绘制。我这个页面上有三处阴影卡片、logo 容器、主按钮。这是我能接受的上限。如果你有三四个按钮都加大阴影低端机上首屏绘制时间可能会翻倍。慎用自定义字体。自定义字体文件动辄几百 KB还要走网络加载加载期间文字要么不显示要么会闪烁。登录页的文字量很小系统默认字体的表现已经够用。真要用也只用在你最在意的那一两个标题上并且必须配font-display: swap之类的兜底。5. 让这个页面更耐看的几个细节前面讲的是怎么让它对这部分讲怎么让它更好。这些细节不写也能用但写了之后页面会从能用变成想截图发群里。5.1 动效的克制动效这块我的观点很明确登录页最多做两处动效多了就是灾难。第一处是页面的入场。表单卡片从下方淡入位移 40rpx透明度从 0 到 1时长 400ms缓动用ease-out。这一个动效就足以让页面显得活起来而且它只在页面加载时播放一次对性能零压力。keyframes cardIn { from { opacity: 0; transform: translateY(40rpx); } to { opacity: 1; transform: translateY(0); } } .form-card { animation: cardIn 0.4s ease-out both; }第二处是按钮的按下反馈。小程序没有原生的按钮按压效果用户点下去如果不给反馈会怀疑没点上。我用hover-class或者主动监听touchstart来做一个轻微缩放.btn-login:active { transform: scale(0.97); box-shadow: 0 6rpx 16rpx rgba(99, 102, 241, 0.24); }scale(0.97)这个值是我试出来的。缩小 5% 不明显10% 又太夸张像果冻。3% 刚好能让用户感知到按下去了同时不会让按钮的圆角变形。千万不要做的那种动效光斑循环漂移、按钮呼吸发光、输入框边框流光。这三样在低端机上都是性能杀手而且看多了很累人。登录页用户一天可能要看十几次花哨的动效第二天就变成烦人。5.2 主题变量与暗色适配如果这套样式你要复用到多个项目建议把颜色抽成变量。uni-app 支持 SCSS可以在uni.scss或者单独的变量文件里定义$brand-primary: #6366f1; $brand-secondary: #8b5cf6; $brand-accent: #d946ef; $text-main: #1f2937; $text-sub: #6b7280; $text-hint: #9ca3af; $surface: rgba(255, 255, 255, 0.72);抽变量的好处不只是改起来方便更重要的是能让整页颜色的明度关系保持一致性。我见过不少页面主色是蓝紫但阴影用的是纯黑、文字用的是纯黑、分割线用的是灰三种深浅各不相干整体看起来很散。变量化之后你能一眼看出哪些地方该用$text-sub而不是随手敲一个#888。关于暗色模式我的建议在登录页上要谨慎。小程序支持prefers-color-scheme媒体查询也能通过app.json配置darkmode。但登录页的玻璃拟态效果是高度依赖背景亮度的浅色的半透明卡片在深色背景上会变成一块惨白的板子非常突兀。要做暗色就得单独设计一套深色下的光斑颜色和卡片透明度工作量不小。我一般的做法是如果是内部工具类小程序不做暗色固定浅色即可但要在app.json里声明强制浅色避免用户在深色系统下看到系统反色导致的各种奇怪问题。如果是面向 C 端、用户会长时间停留的产品那暗色值得单独做一版但请把它当成一个独立的设计任务而不是加几行媒体查询就完事。顺便提一个同样重要的适配大字体模式。不少用户会把系统字体调大。如果你的文字全部用固定 rpx 写死用户调大字体后页面上的文字大小不会变反而显得更小体验是反的。所以在关键的文字节点上字号可以适当用相对单位或者至少保证文字容器有足够的空间余量别让文字一放大就换行溢出容器。这个细节很小但对无障碍体验挺重要的。最后分享一个我在实际项目里用了很久的小习惯把这个登录页做成一个独立的、不依赖任何业务逻辑的示例页面放在pages/demo/login路径下。每次改了样式变量或者封装了新组件先在这个页面里验证效果确认没问题再往业务页面同步。这样做的好处是你永远不会因为改了公共样式而在某个业务页面上引发意外同时它也是给新同事看代码规范的最佳入口。我这套方案从第二篇开始就一直带着这个 demo 页到现在已经积累了七八个风格变体每次新项目开工直接从里面挑一个改配色就能用省下来的时间比我预想的多得多。
返回列表