uni-app 登录页 UI 精修:从背景层到页面栈的细节实战
发布时间:2026/9/30 7:56:29来源:尧图网络
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 classlogin-page__field :class{ login-page__field--active: focusField account, login-page__field--error: !!errors.account } input classlogin-page__input v-modelform.account typetext :maxlength20 placeholder手机号 / 用户名 placeholder-classfield__placeholder :cursor-spacing24 focusonFocus(account) bluronBlur(account) / view v-ifform.account classlogin-page__clear clickclearField(account)×/view /viewconst 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不是typepasswordinput classlogin-page__input v-modelform.password :password!showPwd :maxlength20 placeholder请输入密码 placeholder-classfield__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 而 rejectuni.request在不传回调时会返回 Promise但它有一个很容易踩的点只有网络层失败才会 rejectHTTP 状态码是 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-positionfalse然后自己在focus事件里拿到e.detail.height手动给表单区加一个向上的位移二是把输入框附近的内容做成可滚动的scroll-view让系统自己去推。我一般用第一种因为登录页内容少手动位移更好控制。再配一个:cursor-spacing24保证光标和键盘之间有 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 classlogin-page__logo src/static/logo.png modeaspectFit /.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-typegetPhoneNumber getphonenumberonGetPhone触发的用户点一下授权前端拿到一个动态令牌交给后端换取手机号。要注意三点这个能力需要企业主体并在后台开通个人开发者用不了用户点“拒绝”时回调里拿不到任何数据必须做好降级到账号密码登录的路径这个按钮同样自带默认样式记得::after重置。从转化角度看登录页上把“一键登录”放在最显眼的位置、账号密码作为次要入口实际转化率会明显高于纯表单。但这取决于你的用户群体工具类产品用户更习惯直接登录社交类产品一键登录更顺。7.3 用户头像昵称别塞在登录环节早期用getUserInfo弹窗一次性拿昵称头像的做法已经不再适用现在要用button open-typechooseAvatar配input typenickname让用户主动填。我的建议是登录页只做登录头像和昵称放到“完善资料”那一步而且允许跳过。原因很直白——登录环节每多问一个字段转化率就掉一截用户此刻的心理预期只是“让我进去”。7.4 埋点看清楚用户卡在哪一步登录页是漏斗最上端值得埋的节点有四个页面曝光、验证码按钮点击、登录按钮点击、登录成功/失败。有了这四个点你能算出“输完手机号但没点登录”的比例也能看出失败集中在哪个错误码上。埋点上报只带匿名标识和页面来源千万别把账号、密码、验证码上报上去这属于自己给自己找麻烦。我自己最常看的一个指标是“验证码点击后到登录成功的转化”。如果这个值特别低八成不是 UI 的问题而是短信到达率或者后端校验逻辑太严。UI 做得再好看卡在收不到验证码这一步用户照样跑掉。最后分享一个我在实际项目里坚持的做法登录页写完之后把页面结构、背景层、装饰层的关系在代码里加一行注释说明层级尤其是z-index的分布。因为登录页通常半年才改一次下次打开这个文件的人可能就是你而“点不动”这类问题最先要确认的就是层级关系。把这条注释写上能给自己省下不少回想的时间。
网站建设高端定制企业官网