拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

uni-app 登录页 UI 精修:从背景层到页面栈的细节实战

uni-app 登录页 UI 精修:从背景层到页面栈的细节实战

uni-app 里给微信小程序做登录页面,第一版基本都停留在“能用”的阶段:一个 logo、两个输入框、一个按钮,收工。等产品拿着竞品截图走过来,说一句“这个登录页面 UI 好看,你照着改一下”,很多人才发现自己在 uni-app 里连一个像样的渐变背景叠装饰图形都要来回调半天。这篇是“好看的 UI 登录页”系列的第二篇,第一篇把静态排版、配色、字体层级过了一遍,这篇不谈配色理论,专讲那些“看着只是好看,实际全靠细节堆出来”的部分:背景层怎么叠、输入框要准备几种状态、按钮禁用态怎么做、验证码倒计时怎么避免定时器泄漏、真机上键盘顶起页面怎么处理、登录完成后页面栈该怎么清。

代码按 uni-app + Vue 3 + scss 的组合来写,HBuilderX 3.6 以上直接跑,CLI 工程也一样。适合三类人:刚上手 uni-app 的初中级前端、被 UI 细节反复打回重做的独立开发者、以及一个人把小程序前后端全包了的同学。我会贴完整可跑的代码,但更想讲清楚每一步为什么这么写——照着抄只能解决这一次,弄明白取舍,下次换个设计稿你自己也能推出来。

1. 先把标准定下来:这个登录页要满足哪些条件

1.1 “好看”拆开其实是三层,别一上来就写样式

我习惯把登录页的“好看”拆成三层来看:结构层、色彩层、动效层。结构层指留白、对齐、信息层级,色彩层指主色、渐变、阴影,动效层指聚焦、点击、加载。登录页因为元素极少,结构层的权重占到七成以上——同样一套配色,元素左边缘对不齐、间距忽大忽小,看起来就是廉价;反过来,只用灰白两色但间距严格统一,反而显得克制专业。

所以动手之前先定两条硬规矩。第一条是栅格:所有元素的左边缘对齐到同一个值,我在这个页面里用的是 48rpx;卡片的左右内边距、输入框的左内边距、按钮文字的左内边距全部对齐到这条线。第二条是间距阶梯:只用 8、16、24、32、48、64 这几个值(单位 rpx 的时候放大十倍),禁止出现 13、27 这种随手写的数字,这类数字是页面显得“脏”的第一元凶。把手感量化成规则之后,改稿效率会高很多,产品说“再松一点”,你知道改的是阶梯而不是拍脑袋。

1.2 一套代码要跑在几个端,差异提前列出来

uni-app 的优势是一套代码多端跑,但登录页恰好是最容易暴露端差异的页面,因为它是用户进入的第一个交互屏。微信小程序端要处理自定义导航栏后的状态栏高度;H5 端要处理浏览器地址栏导致的 100vh 不准、以及移动端键盘行为不一致;App 端要处理底部安全区和返回键逻辑。渲染层面的差异更要提前知道:小程序在不同渲染模式下对backdrop-filter、filter: blur()的支持程度不一样,低端安卓机上的表现和开发者工具里完全是两回事。

我的做法是在项目根目录建一个styles/目录,放variables.scss、mixins.scss、common.scss三个文件,端差异全部用条件编译隔离在 mixin 里。比如安全区适配写成一个@mixin safe-bottom,小程序和 App 走env(safe-area-inset-bottom),H5 走普通 padding。这样页面样式里不会到处是#ifdef,可读性能保住。

1.3 技术选型上我踩过的三个坑

第一个坑是组件库。刚做 uni-app 的时候我习惯性引入组件库,想着输入框、按钮直接用现成的省事,结果登录页这个尺寸下组件库反而是负担:默认内边距、默认行高、默认边框全都要覆盖,覆盖样式比从零写还多,而且版本升级后样式还可能变。所以登录页我建议直接用原生view/input/image写,不引入组件库。

第二个坑是单位。主尺寸用 rpx 没问题,750rpx 等于屏幕宽,1rpx 在 iPhone 6 上刚好是 0.5 物理像素。但字号我建议用 px 固定,别用 rpx——用 rpx 的话,iPad 或者折叠屏上标题会大得离谱。第三个坑是 1px 边框:很多人习惯写border: 1rpx solid #eee,在部分安卓机上这条线会直接消失,因为算出来的物理像素小于 1。稳妥做法是直接写1px,或者用伪元素加transform: scaleY(0.5)。

提醒:一个页面里不要同时出现 rpx 和 px 混着做主尺寸,改稿时会疯。我自己的约定是“尺寸用 rpx、字号和边框用 px”。

2. 页面骨架:三区结构、pages.json 配置与命名约定

2.1 背景层、品牌区、表单区,三块各管各的

这个登录页我拆成三块。背景层负责氛围,用绝对定位铺满,z-index: 0,不参与文档流,也不绑定任何事件;品牌区放 logo 和欢迎语,高度约占屏幕的三分之一;表单区是主体,包含输入框、协议勾选、登录按钮、第三方登录入口。三块都用 flex 布局,页面容器min-height: 100vh加box-sizing: border-box。

这里有个细节值得说:不要直接给容器写height: 100vh。小程序里 100vh 在部分机型会包含状态栏区域,导致内容被顶下去一截或者底部溢出。用min-height更安全,内容超过一屏时也不会被裁掉。装饰元素全部放在背景层里,绝不允许它出现在表单层之上——这一点后面讲排查的时候会看到,装饰层盖住按钮是登录页最经典的“点击没反应”事故。

2.2 pages.json 里那几个必须改的字段

{ "path": "pages/login/login", "style": { "navigationStyle": "custom", "backgroundColor": "#f7f8fc", "backgroundColorTop": "#f7f8fc", "backgroundColorBottom": "#f7f8fc", "disableScroll": true } }

navigationStyle设成custom是为了去掉小程序默认导航栏,这样整个屏幕都是你的画布,渐变背景能一路铺到顶部。代价是状态栏区域要自己留高度,我是这么取的:

import { ref, onMounted } from 'vue' const statusBarHeight = ref(20) onMounted(() => { const info = uni.getWindowInfo ? uni.getWindowInfo() : uni.getSystemInfoSync() statusBarHeight.value = info.statusBarHeight || 20 })

disableScroll: true是为了防止键盘弹起时页面整体被推走——登录页内容不多,不需要滚动,禁掉能省掉一堆错位问题。backgroundColor这三个字段很多人不写,结果是用户往下拉或者键盘收起时,露出来的是系统默认的白色,和你的渐变背景拼在一起非常割裂。

2.3 class 命名和 scoped 的真实边界

命名我用 BEM 的简化版:块名login-page,元素名login-page__form,状态修饰符login-page__field--active。好处是团队协作时一眼能看出从属关系,而且样式文件里的嵌套层级不会失控。

但有个必须提前知道的坑:uni-app 编译到小程序时,scoped是靠给元素注入自定义属性实现的,对小程序内置组件的内部节点管不到。最典型的就是输入框的占位符样式——你在 scoped 里写.field__placeholder怎么都不生效。这个样式必须放到全局,比如 App.vue 里或者一个不加 scoped 的公共样式文件里:

/* App.vue 的 style 里,不加 scoped */ .field__placeholder { color: #b6bccb; font-size: 28rpx; }

同理,需要改组件库或内置组件内部结构时,得用:deep()。这不是玄学,记住“scoped 只管自己模板里直接写出来的元素”就行。

3. 视觉层实现:背景、输入框状态、按钮与适配

3.1 渐变背景和装饰光斑,别无脑上高斯模糊

背景我用的是一段三段式线性渐变,从上到下从浅蓝过渡到接近白,这样顶部品牌区有颜色、底部表单区干净,视线自然往下走:

.login-page { position: relative; min-height: 100vh; box-sizing: border-box; background: linear-gradient(180deg, #eef3ff 0%, #f7f8fc 42%, #ffffff 100%); overflow: hidden; } .login-page__blob { position: absolute; z-index: 0; border-radius: 50%; pointer-events: none; } .login-page__blob--a { width: 520rpx; height: 520rpx; top: -180rpx; right: -160rpx; background: radial-gradient(circle, rgba(122, 162, 255, 0.55) 0%, rgba(122, 162, 255, 0) 70%); } .login-page__blob--b { width: 420rpx; height: 420rpx; top: 220rpx; left: -200rpx; background: radial-gradient(circle, rgba(160, 214, 255, 0.5) 0%, rgba(160, 214, 255, 0) 70%); }

重点说光斑的实现方式。网上很多写法是给圆形加filter: blur(80rpx),在开发者工具里效果很好,但到真机上,尤其是两三年前的安卓中端机,滚动或者键盘弹起时明显掉帧,因为高斯模糊是实时计算的。我后来换成radial-gradient从中心色到全透明,视觉上几乎一样,但渲染成本低得多,纯静态绘制,不需要实时算模糊。这个替换是我自己在真机对比之后改的,不是理论上更优,是实测更稳。

注意:光斑属于装饰,千万不要把它放在表单层上方,也不要在它身上绑@click。跨端排查触碰事件优先级很麻烦,让它待在底层最省心。

3.2 输入框要准备四种状态,焦点态只能靠 JS 驱动

一个输入框至少要有四种视觉状态:默认、聚焦、已填写(有内容时分隔线变亮、右侧出现清除按钮)、错误。默认态就是浅灰底、圆角、无边框。这里的关键点是:小程序样式里没有:focus-within这类选择器,所以“父容器在内部 input 聚焦时改变自身样式”这件事只能靠 JS 变量驱动 class。

<view class="login-page__field" :class="{ 'login-page__field--active': focusField === 'account', 'login-page__field--error': !!errors.account }" > <input class="login-page__input" v-model="form.account" type="text" :maxlength="20" placeholder="手机号 / 用户名" placeholder-class="field__placeholder" :cursor-spacing="24" @focus="onFocus('account')" @blur="onBlur('account')" /> <view v-if="form.account" class="login-page__clear" @click="clearField('account')">×</view> </view>
const focusField = ref('') const onFocus = (key) => { focusField.value = key } const onBlur = (key) => { if (focusField.value === key) focusField.value = '' validateField(key) // 失焦才校验,输入过程中不打扰 }

四个状态之间的过渡用transition: border-color .2s, background-color .2s就够,别加太多动效。密码框额外加一个眼睛图标切换明文/密文,注意 uni-app 的input用的是布尔属性password,不是type="password":

<input class="login-page__input" v-model="form.password" :password="!showPwd" :maxlength="20" placeholder="请输入密码" placeholder-class="field__placeholder" />

maxlength一定要设。它不只是体验问题,也直接关系到小程序的 setData 数据量——每敲一个字符都会触发一次数据同步,长度不限制的话,用户粘贴一大段文本进来,页面立刻卡给你看。

3.3 按钮用 view 还是 button,取决于你要不要 open-type

纯粹提交表单的登录按钮,我建议用view。原因很实际:小程序的button组件自带一套默认样式,包括那个通过::after画出来的边框,覆盖起来要写button::after { border: none; },还经常因为权重问题失败。用view自己写,可控性最高。

但如果你要做手机号快捷验证或者获取用户头像,那就必须用button并且带open-type,这时候老老实实重置样式:

.login-page__btn { height: 96rpx; line-height: 96rpx; border-radius: 48rpx; text-align: center; font-size: 32rpx; color: #fff; background: linear-gradient(135deg, #5b8cff 0%, #3f6bff 100%); box-shadow: 0 12rpx 28rpx rgba(63, 107, 255, 0.28); transition: opacity .2s, transform .1s; } .login-page__btn::after { border: none; } .login-page__btn--disabled { background: #c9d3ea; box-shadow: none; opacity: .9; } .login-page__btn--pressed { transform: scale(.98); }

禁用态为什么要单独写,而不是靠:disabled?因为小程序里给view加不了disabled,给button加disabled后默认样式是灰底灰字,跟你的渐变风格冲突。所以统一用变量控制 class,同时在点击处理函数的第一行做拦截:

const submitting = ref(false) const handleLogin = async () => { if (submitting.value) return // 防重复点击 if (!canSubmit.value) { uni.showToast({ title: '请先完成表单填写', icon: 'none' }) return } // ... }

submitting这个锁很关键。用户的习惯是点一次没反应就拼命点,如果接口慢,就变成连发五个登录请求,后端日志里一片狼藉,严重的话还会触发风控。锁必须在finally里复位,而不是在then里,否则请求报错后按钮会永远卡在加载态。

3.4 rpx、字号、底部安全区,三个容易被忽略的值

尺寸用 rpx、字号用 px 这条前面说过,补充一个判断依据:凡是需要“跟着屏幕变宽等比放大”的,用 rpx(卡片宽度、图标尺寸、间距、圆角);凡是需要“人眼看着舒服而不是等比缩放”的,用 px(字号、行高、边框)。底部安全区用一行搞定:

.login-page__footer { padding-bottom: calc(32rpx + env(safe-area-inset-bottom)); }

env(safe-area-inset-bottom)在小程序和 App 上都支持,H5 上取不到值时会退化成 0,正好。还有一个细节是小数 rpx:像0.5rpx这种值,编译后可能被四舍五入成 0,等于没写,别用。

4. 逻辑层实现:校验、倒计时、请求与登录态

4.1 表单状态和校验时机,别在输入过程中报错

import { reactive, ref, computed } from 'vue' const form = reactive({ account: '', password: '', code: '' }) const errors = reactive({ account: '', password: '' }) const agreed = ref(false) const rules = { account: { reg: /^1[3-9]\d{9}$/, msg: '请输入正确的手机号' }, password: { reg: /^\S{6,20}$/, msg: '密码为 6-20 位且不含空格' } }

校验规则和触发时机我整理成一张表,照着做体验会明显好一截:

字段规则触发时机说明
手机号^1[3-9]\d{9}$失焦时只在失焦时提示,输入中不打扰
密码6-20 位非空白字符失焦时同上,但已报错后随输入实时清除
协议勾选必须为 true点击登录时不勾选时抖动一下协议行并提示
验证码4-6 位数字失焦时我这里放的是图形验证码,短信码逻辑同理

这里有个我调过很多次才定下来的细节:字段已经处于错误状态时,用户在输入过程中就应该实时清掉错误提示,而不是等下次失焦。因为人看到红色报错之后会立刻修改,如果红色一直挂着不动,会怀疑自己是不是没改对。实现就是@input里判断if (errors[key]) errors[key] = '',一行代码,反馈感完全不同。

正则也要注意,网上很多老教程写的是^1[3578]\d{9}$,这个表达式会把 19x、16x 等一批号段全部判为非法,用户填了正确手机号却提示格式错误,这类工单特别难查。老老实实用^1[3-9]\d{9}$。

4.2 验证码倒计时:用时间戳算,别用自减

倒计时看着简单,实际上是最容易写出 bug 的地方。新手写法是countdown--,问题在于setInterval本身不精确,加上小程序切后台、页面被覆盖时定时器还会继续跑,用户切出去十秒再回来,数字就乱了。正确做法是记录一个结束时间戳,每次 tick 用当前时间反推剩余秒数:

let timer = null const counting = ref(false) const countdown = ref(0) let endTime = 0 const startCountdown = () => { endTime = Date.now() + 60 * 1000 counting.value = true countdown.value = 60 clearInterval(timer) timer = setInterval(() => { const left = Math.ceil((endTime - Date.now()) / 1000) if (left <= 0) { clearInterval(timer) timer = null counting.value = false countdown.value = 0 } else { countdown.value = left } }, 500) } onUnmounted(() => { if (timer) { clearInterval(timer); timer = null } })

两个要点。第一,间隔用 500ms 而不是 1000ms,因为用时间戳计算的前提下,多跑几次不会导致数字跳错,反而能让状态切换更即时。第二,onUnmounted里的清理不能省,页面销毁时定时器还在跑,轻则内存泄漏,重则在页面已经不存在时执行赋值,控制台报一堆警告。如果页面可能长时间被覆盖,可以再加一层onHide/onShow的控制,让页面隐藏时停掉计时、显示时按时间戳恢复——因为恢复逻辑是基于时间戳的,切回来数字天然是对的。

4.3 登录请求:注意 uni.request 不会因为 500 而 reject

uni.request在不传回调时会返回 Promise,但它有一个很容易踩的点:只有网络层失败才会 reject,HTTP 状态码是 500 也会走成功分支。所以判断必须自己写:

const BASE_URL = 'https://api.example.com' const handleLogin = async () => { if (submitting.value) return if (!agreed.value) { shakeAgreement() return } submitting.value = true uni.showLoading({ title: '登录中', mask: true }) try { const res = await uni.request({ url: `${BASE_URL}/auth/login`, method: 'POST', data: { account: form.account, password: form.password }, timeout: 10000, header: { 'content-type': 'application/json' } }) if (res.statusCode !== 200 || !res.data || res.data.code !== 0) { uni.showToast({ title: (res.data && res.data.message) || '登录失败', icon: 'none' }) return } persistLogin(res.data.data) } catch (e) { uni.showToast({ title: '网络异常,请稍后重试', icon: 'none' }) } finally { uni.hideLoading() submitting.value = false } }

mask: true的 loading 有两个作用:一是告诉用户操作已经被接收,二是挡住重复点击。不过要注意,uni.showLoading和uni.showToast是互斥的,Toast 会把 Loading 顶掉,所以hideLoading必须在 Toast 之前或者用finally兜住,不然会出现 loading 一直在转的假象。

请求头带 token 这类公共逻辑,建议用拦截器统一处理,别在每个页面里重复写:

uni.addInterceptor('request', { invoke(args) { const token = uni.getStorageSync('token') if (token) { args.header = Object.assign({}, args.header, { Authorization: `Bearer ${token}` }) } return args } })

4.4 登录态落地与跳转,reLaunch 是重点

登录成功之后先落盘再跳转:

const persistLogin = (data) => { uni.setStorageSync('token', data.token) uni.setStorageSync('userInfo', { nickname: data.nickname, avatar: data.avatar }) uni.reLaunch({ url: '/pages/index/index' }) }

跳转一定用uni.reLaunch而不是navigateTo。navigateTo是压栈,登录页还在页面栈里,用户点左上角返回就又回到登录页,然后他再点一次登录,栈里就有两份首页,越堆越乱。reLaunch会关掉所有页面再打开目标页,从根上解决这个问题。

自动登录的时机放在 App.vue 的onLaunch里:读本地 token,带着 token 请求一次用户信息接口,成功就直接reLaunch到首页,失败就清掉 token 停在登录页。这里有个体验细节,自动登录的过程不要弹 loading,而是在首页做一个骨架屏或者启动图过渡,否则用户会看到一个转圈一闪而过,反而显得迟钝。

提醒:token 存在本地是明文可读的,前端做任何自创的“加密存储”都拦不住有心人。前端能做的只是不把敏感信息(身份证、完整手机号、支付信息)往本地存;真正的权限判断必须在服务端做,这一点别抱侥幸。

5. 体验与性能:让好看的页面别卡

5.1 键盘遮挡和输入卡顿的处理

小程序的input有个adjust-position属性,默认是 true,键盘弹起时页面整体上推。如果你的登录页是整屏 flex 布局,上推之后内容会被顶出可视区域,用户看不到自己输入的内容。两种解法:一是设成:adjust-position="false",然后自己在@focus事件里拿到e.detail.height,手动给表单区加一个向上的位移;二是把输入框附近的内容做成可滚动的scroll-view,让系统自己去推。

我一般用第一种,因为登录页内容少,手动位移更好控制。再配一个:cursor-spacing="24",保证光标和键盘之间有 24px 的呼吸空间,不会贴着键盘顶边。

输入卡顿的根因基本都指向 setData 过于频繁。除了前面说的限制maxlength,还要注意两点:一是不在@input里做重计算,比如实时请求接口查用户名是否存在,这种事放到失焦或者加 300ms 防抖;二是不要在同一个输入框上同时绑v-model和多个依赖它的计算属性,每次输入都会触发一串依赖更新。真机实测下来,把校验从 input 挪到 blur,输入框的跟手感会有明显改善。

5.2 包体积:登录页最容易超标的就是图片和图标字体

小程序整个包有体积限制,主包 2MB,分包总和 20MB,登录页作为首屏页面通常放在主包里,它的大小直接影响主包能不能过。三个省体积的方向:

第一个是 logo 和背景图。超过 20KB 的图坚决上 CDN,本地只留一个几十 KB 的小 logo;如果 logo 是纯色简单图形,转 base64 内联反而更划算,能省掉一个网络请求,首屏不会闪。

第二个是字体图标。小程序 wxss 里的url()不支持引用本地字体文件,必须用 base64 内联或者 https 地址。整套 iconfont 的 ttf 转 base64 随随便便就是 100KB 往上,为了登录页三个图标付这个代价不值。登录页图标少的时候,我的选择是:能用纯 CSS 画的就画(比如用两个旋转 45 度的方块拼一个叉),不能画的切小尺寸 PNG,一张 2KB 左右,三张也比字体包小。

第三个是 easycom 扫描范围。如果项目里配了 easycom,注意规则别写得太宽泛,否则编译时会扫描大量无用组件;这个对运行体积影响不大,但对编译速度和开发者工具的内存占用影响挺明显。

5.3 首屏渲染:别让背景图加载导致布局跳动

登录页是用户打开小程序看到的第一屏,最忌讳的是元素“跳一下”。原因是图片没有预设尺寸,加载完成前高度是 0,加载完后把下面的内容全推下去。解决办法很土但有效:给所有image写死宽高,mode用aspectFit,这样即使图还没到,占位空间也是对的。

<image class="login-page__logo" src="/static/logo.png" mode="aspectFit" />
.login-page__logo { width: 160rpx; height: 160rpx; }

至于骨架屏,登录页我一般不加。内容太少,骨架屏一闪而过反而添乱。真正需要骨架屏的是登录后的首页数据列表,那是另一个话题。

6. 常见问题与排查实录

6.1 样式不生效的六种典型情况

现象真实原因处理方式
占位符颜色改不动placeholder-class的样式在 scoped 里不生效放到全局样式或 App.vue
按钮四周有一圈细线小程序 button 自带::after边框button::after { border: none }
安卓上细分割线消失1rpx 计算后小于 1 物理像素改 1px 或用伪元素scaleY(0.5)
毛玻璃效果无效部分安卓机型或特定渲染模式不支持backdrop-filter退化成半透明纯色背景
页面滚动掉帧装饰元素用了filter: blur()换成radial-gradient模拟光斑
顶部内容被状态栏压住自定义导航栏后没自己留状态栏高度uni.getWindowInfo().statusBarHeight

这六条里有四条我在真机上真金白银踩过。特别是遮挡和毛玻璃这两条,在开发者工具里完全看不出问题,一定要上真机验证。

6.2 开发者工具正常、真机异常的排查顺序

遇到“工具里好好的,真机不对”,我固定按这个顺序查:先看是不是渲染模式差异(切换一下渲染模式对比),再看是不是 CSS 特性不支持(逐条注释掉嫌疑样式二分定位),然后看是不是字体和行高差异(iOS 和安卓默认字体不同,行高能差 2-4px,所以别把行高写死成固定值,用line-height: 1.4这种倍数),最后看是不是机型适配(小屏、大屏各来一台)。

最省事的验证方式是用真机调试,而不是扫码预览,真机调试能看到 console 里的警告,很多 CSS 不支持的问题会直接打印出来。

6.3 交互层问题速查表

问题排查顺序
点登录按钮没反应① 是不是被装饰层盖住 ②submitting是否卡在 true 没复位 ③ 事件是否写成了@click而不是onClick
倒计时数字乱跳用了自减而不是时间戳;或者定时器没在页面销毁时清掉
键盘弹起页面错乱adjust-position与固定定位冲突,改成手动位移
登录成功后返回又回到登录页跳转用了navigateTo,改成reLaunch
明明输对了手机号却提示格式错误正则号段太老,改成^1[3-9]\d{9}$
验证码一直收不到后端限流、正则拦掉了新号段、或者请求被缓存

这张表我基本是照着工单总结的,出现频率最高的还是第一条和第四条。第一条尤其隐蔽,因为装饰层没有绑任何事件,你在代码里搜事件绑定搜不出问题,只能靠z-index层级去查。

7. 后续还能怎么扩展

7.1 主题换肤:用 CSS 变量而不是 scss 变量

scss 变量是编译期的,改主题要重新编译;CSS 变量是运行期的,在根节点上换一个 class 就能整套换色。登录页如果要做深色模式,把颜色抽成 CSS 变量是最省事的路线:

.login-page { --brand: #3f6bff; --brand-soft: #eef3ff; --text-main: #1c2233; --text-sub: #8792a8; --field-bg: #f4f6fb; } .login-page--dark { --brand: #6f96ff; --brand-soft: #1a2036; --text-main: #eef1f8; --text-sub: #8c96ad; --field-bg: #232a42; }

组件里的颜色全部引用变量,换肤就变成加一个 class 的事。深色模式下要多留一个心眼:阴影在深色背景上几乎看不见,需要用边框或更亮的背景色来做层级区分,不能照搬浅色方案。

7.2 一键登录和手机号快捷验证

手机号快捷验证是通过<button open-type="getPhoneNumber" @getphonenumber="onGetPhone">触发的,用户点一下授权,前端拿到一个动态令牌,交给后端换取手机号。要注意三点:这个能力需要企业主体并在后台开通,个人开发者用不了;用户点“拒绝”时回调里拿不到任何数据,必须做好降级到账号密码登录的路径;这个按钮同样自带默认样式,记得::after重置。

从转化角度看,登录页上把“一键登录”放在最显眼的位置、账号密码作为次要入口,实际转化率会明显高于纯表单。但这取决于你的用户群体,工具类产品用户更习惯直接登录,社交类产品一键登录更顺。

7.3 用户头像昵称别塞在登录环节

早期用getUserInfo弹窗一次性拿昵称头像的做法已经不再适用,现在要用<button open-type="chooseAvatar">配<input type="nickname">让用户主动填。我的建议是:登录页只做登录,头像和昵称放到“完善资料”那一步,而且允许跳过。原因很直白——登录环节每多问一个字段,转化率就掉一截,用户此刻的心理预期只是“让我进去”。

7.4 埋点:看清楚用户卡在哪一步

登录页是漏斗最上端,值得埋的节点有四个:页面曝光、验证码按钮点击、登录按钮点击、登录成功/失败。有了这四个点,你能算出“输完手机号但没点登录”的比例,也能看出失败集中在哪个错误码上。埋点上报只带匿名标识和页面来源,千万别把账号、密码、验证码上报上去,这属于自己给自己找麻烦。

我自己最常看的一个指标是“验证码点击后到登录成功的转化”。如果这个值特别低,八成不是 UI 的问题,而是短信到达率或者后端校验逻辑太严。UI 做得再好看,卡在收不到验证码这一步,用户照样跑掉。

最后分享一个我在实际项目里坚持的做法:登录页写完之后,把页面结构、背景层、装饰层的关系在代码里加一行注释说明层级,尤其是z-index的分布。因为登录页通常半年才改一次,下次打开这个文件的人可能就是你,而“点不动”这类问题,最先要确认的就是层级关系。把这条注释写上,能给自己省下不少回想的时间。

返回列表