☰
基于 element UI 的修改密码页面设计:加载态与全局异常捕捉实战
2026/10/15 12:57:11 网站建设 项目流程

1. 从一个后台管理页面的“小需求”说起

做后台管理系统的人大概都有这种体会:登录、改密码、异常提示这几个页面,看起来是整个项目里最没技术含量的部分,但真正落到实际项目里,它们往往是最容易出问题、最容易被反复返工的地方。我最近接手的一个后台项目,需求方只提了一句话——“加一个修改密码的页面,密码规则按公司安全规范来,页面加载的时候别白屏,出错要有统一提示”。听起来是不是特别简单?但真动手做的时候,你会发现这里面牵扯到的东西一点都不少:表单校验规则怎么设计、新旧密码一致性怎么校验、提交时的加载态怎么处理、接口异常怎么全局兜底、用户连续点击怎么防抖、密码强度提示怎么实时反馈、修改成功后是跳登录页还是留在原地……每一个细节都值得单独拎出来说。

这篇内容就是围绕“基于 element UI 修改密码接口页面设计,加载页面,异常全局捕捉”这个主题,把我实际做这个页面的完整思路、踩过的坑、以及可以直接抄作业的代码结构整理出来。核心关键词包括element UI 表单校验、修改密码接口设计、页面加载态处理、全局异常捕捉、axios 拦截器、密码强度校验。适合正在做后台管理系统、需要实现类似功能的前端开发者,也适合想系统梳理“表单+接口+异常处理”这条链路的同学。不管你是刚入行的新手,还是做了几年但一直没认真整理过这套流程的老手,应该都能从里面找到点有用的东西。

我先把结论摆在这儿:修改密码这个页面,难点从来不在 UI 本身,而在于状态管理和异常边界。element UI 已经把表单组件给你封装得很好了,你真正要花心思的是——用户从进入页面到提交成功这整个过程中,每一个可能出错的环节,你都要提前想到并且给出合理的反馈。下面我按模块拆开讲。

2. 修改密码页面的整体设计与思路拆解

2.1 为什么选择 element UI 的 el-form 作为基础

后台管理系统用 element UI 基本是行业默认选择,尤其是 Vue2 技术栈的项目。修改密码这个页面本质上是一个三字段表单:原密码、新密码、确认新密码。用el-form配合el-form-item和el-input,能直接拿到校验、错误提示、布局对齐这些能力,没必要自己造轮子。

但这里有个选型上的细节值得说:很多人习惯用el-form的rules做校验,然后在提交时手动再校验一遍。我的做法是把校验规则集中定义在一个独立的 rules 对象里,而不是散落在 template 中。原因是修改密码的规则通常会跟公司安全策略挂钩,比如“新密码不能与原密码相同”“新密码必须包含大小写字母和数字”“长度 8-20 位”,这些规则未来可能会调整,集中管理方便维护。

// rules.js export const passwordRules = { oldPassword: [ { required: true, message: '请输入原密码', trigger: 'blur' } ], newPassword: [ { required: true, message: '请输入新密码', trigger: 'blur' }, { min: 8, max: 20, message: '密码长度需在 8 到 20 位之间', trigger: 'blur' }, { pattern: /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).+$/, message: '需包含大小写字母和数字', trigger: 'blur' }, { validator: validateNotSameAsOld, trigger: 'blur' } ], confirmPassword: [ { required: true, message: '请再次输入新密码', trigger: 'blur' }, { validator: validateConfirm, trigger: 'blur' } ] }

这里有个关键点:“新密码不能与原密码相同”这个校验,必须放在 newPassword 的 validator 里,而不是 confirmPassword 里。因为用户可能先填新密码再填确认密码,如果校验逻辑挂错了字段,会出现“确认密码填对了但提示新密码有问题”的错位感。我见过不少项目在这里翻车。

2.2 页面加载态到底在加载什么

“加载页面”这个词听起来有点模糊,实际上在修改密码这个场景里,加载态分两种:

第一种是路由进入时的页面级加载。如果这个页面需要先请求一些初始化数据(比如获取当前用户的密码策略配置、密码过期时间等),那就需要在created或mounted里发请求,同时用一个loading变量控制v-loading指令。element UI 的v-loading可以直接挂在容器上,非常方便。

第二种是提交时的按钮级加载。用户点击“确认修改”后,按钮要变成 loading 状态并禁用,防止重复提交。这个用el-button的:loading属性就能搞定。

我见过有人把这两种 loading 混在一起用同一个变量,结果页面初始化的时候按钮也在转,体验很怪。建议用两个独立变量:pageLoading和submitLoading。

2.3 异常全局捕捉为什么必须做

修改密码接口的异常情况比普通查询接口多得多,常见的就有:原密码错误、新密码不符合策略、两次密码不一致、账号被锁定、接口超时、网络断开、服务端 500。如果每个页面都自己写 try-catch,代码会非常冗余,而且容易漏。

所以我的方案是在 axios 层面做统一拦截,业务代码里只关心成功回调,异常统一由拦截器处理并弹出提示。这样修改密码页面里的提交逻辑可以写得非常干净:

async handleSubmit() { await this.$refs.passwordForm.validate() this.submitLoading = true try { await updatePassword({ oldPassword: this.form.oldPassword, newPassword: this.form.newPassword }) this.$message.success('密码修改成功,请重新登录') this.$router.push('/login') } finally { this.submitLoading = false } }

注意这里我用了finally而不是catch,因为异常已经被拦截器处理了,业务层不需要再弹一次错误提示,只需要保证 loading 状态被正确关闭。这个细节很多人会忽略,导致接口报错后按钮一直转圈。

3. 核心细节解析与实操要点

3.1 密码强度实时反馈怎么做才不烦人

element UI 本身没有密码强度组件,但可以用el-progress或者简单的色块自己实现。我的做法是在新密码输入框下方放一个三格强度条,根据密码的字符类型数量动态变色。

computed: { passwordStrength() { const pwd = this.form.newPassword if (!pwd) return { level: 0, text: '', color: '' } let score = 0 if (pwd.length >= 8) score++ if (/[a-z]/.test(pwd) && /[A-Z]/.test(pwd)) score++ if (/\d/.test(pwd)) score++ if (/[^a-zA-Z0-9]/.test(pwd)) score++ const map = { 0: { text: '弱', color: '#f56c6c' }, 1: { text: '弱', color: '#f56c6c' }, 2: { text: '中', color: '#e6a23c' }, 3: { text: '强', color: '#67c23a' }, 4: { text: '强', color: '#67c23a' } } return { level: score, ...map[score] } } }

这里有个实操心得:强度提示不要用弹窗或者 toast,一定要内联在输入框下方。因为用户输入密码时注意力在输入框上,弹窗会打断输入节奏。另外强度文案不要写“您的密码强度为中等,建议增加特殊字符”这种长句子,直接一个“中”字加颜色就够了,简洁有效。

3.2 新旧密码一致性校验的坑

“新密码不能与原密码相同”这个校验,看起来简单,但有个隐藏问题:如果用户先填了新密码,再回头改原密码,此时新密码的校验不会自动触发。因为 element UI 的校验是绑定在字段的 trigger 上的,原密码变化不会触发新密码字段的重新校验。

解决办法是在原密码的change或blur事件里,手动触发新密码字段的校验:

handleOldPasswordBlur() { if (this.form.newPassword) { this.$refs.passwordForm.validateField('newPassword') } }

这个细节非常容易被忽略,但不处理的话,用户可能提交时才发现“新密码和原密码相同”,体验很差。

3.3 确认密码的校验时机

确认密码的校验有个经典问题:用户先填确认密码,再改新密码,此时确认密码的校验不会自动更新。所以同样需要在newPassword的change事件里触发confirmPassword的校验。

另外,确认密码的 validator 里要同时判断“是否为空”和“是否与新密码一致”,但 element UI 的required规则和自定义 validator 可能会重复提示。我的做法是把 required 和一致性校验合并到一个 validator 里,避免出现两条错误提示。

const validateConfirm = (rule, value, callback) => { if (!value) { callback(new Error('请再次输入新密码')) } else if (value !== this.form.newPassword) { callback(new Error('两次输入的密码不一致')) } else { callback() } }

注意:validator 里如果用箭头函数,this指向的是组件实例,可以直接访问this.form。如果用普通函数,需要提前把this存下来。

3.4 提交前的最后一道防线

即使表单校验通过了,提交前我还会做一次手动兜底校验。原因是有些边界情况 element UI 的自动校验覆盖不到,比如用户通过浏览器自动填充插件填入了不符合规则的内容,或者用户用开发者工具改了 DOM。手动校验的逻辑很简单:

const finalCheck = () => { if (!this.form.oldPassword || !this.form.newPassword) { this.$message.warning('请完整填写密码信息') return false } if (this.form.newPassword !== this.form.confirmPassword) { this.$message.warning('两次输入的密码不一致') return false } return true }

这道防线看起来多余,但在实际项目里救过我好几次。尤其是对接一些老系统时,前端校验和后端校验规则不一致,手动兜底能提前拦截一部分明显错误,减少无效请求。

4. 实操过程与核心环节实现

4.1 页面完整结构搭建

先看 template 的整体结构。我用的是el-card包裹表单,这样在后台管理系统的内容区里视觉上比较统一。

<template> <div class="password-page" v-loading="pageLoading"> <el-card class="password-card"> <div slot="header" class="card-header"> <span>修改密码</span> </div> <el-form ref="passwordForm" :model="form" :rules="rules" label-width="100px" @submit.native.prevent > <el-form-item label="原密码" prop="oldPassword"> <el-input v-model="form.oldPassword" type="password" show-password placeholder="请输入原密码" @blur="handleOldPasswordBlur" /> </el-form-item> <el-form-item label="新密码" prop="newPassword"> <el-input v-model="form.newPassword" type="password" show-password placeholder="8-20位,含大小写字母和数字" @input="handleNewPasswordInput" /> <div v-if="form.newPassword" class="strength-bar"> <span v-for="i in 3" :key="i" class="strength-segment" :style="{ background: i <= strengthLevel ? strengthColor : '#e4e7ed' }" /> <span class="strength-text" :style="{ color: strengthColor }"> {{ strengthText }} </span> </div> </el-form-item> <el-form-item label="确认密码" prop="confirmPassword"> <el-input v-model="form.confirmPassword" type="password" show-password placeholder="请再次输入新密码" /> </el-form-item> <el-form-item> <el-button type="primary" :loading="submitLoading" @click="handleSubmit" > 确认修改 </el-button> <el-button @click="handleReset">重置</el-button> </el-form-item> </el-form> </el-card> </div> </template>

这里有几个细节值得展开说。第一,@submit.native.prevent是为了防止表单默认提交行为导致页面刷新,虽然 el-form 本身不会触发原生提交,但加上更保险。第二,show-password是 element UI 自带的密码可见切换,不用自己写眼睛图标。第三,强度条我用的是三个 span 色块,比 el-progress 更轻量,视觉上也更符合“密码强度”这个语义。

4.2 接口层设计与全局异常拦截

接口层我单独抽了一个api/user.js,修改密码的请求放在这里:

import request from '@/utils/request' export function updatePassword(data) { return request({ url: '/api/user/password', method: 'put', data }) }

重点是utils/request.js里的拦截器。这是全局异常捕捉的核心:

import axios from 'axios' import { Message } from 'element-ui' import router from '@/router' const service = axios.create({ baseURL: process.env.VUE_APP_BASE_API, timeout: 15000 }) service.interceptors.request.use( config => { const token = localStorage.getItem('token') if (token) { config.headers['Authorization'] = `Bearer ${token}` } return config }, error => Promise.reject(error) ) service.interceptors.response.use( response => { const res = response.data if (res.code !== 200) { Message.error(res.message || '请求失败') if (res.code === 401) { router.push('/login') } return Promise.reject(new Error(res.message || 'Error')) } return res.data }, error => { let message = '网络异常,请稍后重试' if (error.response) { const status = error.response.status const statusMap = { 400: '请求参数有误', 401: '登录已过期,请重新登录', 403: '没有操作权限', 404: '请求的资源不存在', 500: '服务器内部错误', 502: '网关错误', 503: '服务暂不可用' } message = statusMap[status] || `请求失败(${status})` if (status === 401) { localStorage.removeItem('token') router.push('/login') } } else if (error.code === 'ECONNABORTED') { message = '请求超时,请检查网络后重试' } Message.error(message) return Promise.reject(error) } ) export default service

这段代码里有几个关键设计。第一,业务错误码和 HTTP 状态码分开处理。业务错误码在第一个if里判断,HTTP 错误在error回调里判断。第二,401 统一跳登录页,并且清除本地 token,避免用户拿着过期 token 反复请求。第三,超时单独处理,因为超时和网络断开的提示文案不一样,用户体验上要区分开。

提示:Message 组件在拦截器里调用时,如果短时间内多次触发,会出现多个提示堆叠。可以在拦截器里加一个简单的节流,或者用Message.closeAll()先关闭再弹。

4.3 页面初始化加载逻辑

如果修改密码页面需要拉取密码策略配置,可以在created里发请求:

created() { this.initPage() }, methods: { async initPage() { this.pageLoading = true try { const config = await getPasswordPolicy() this.policy = config this.rules = buildRules(config) } finally { this.pageLoading = false } } }

这里用finally保证无论请求成功还是失败,loading 都会被关闭。如果请求失败,拦截器已经弹了错误提示,页面会展示默认的校验规则,用户仍然可以尝试修改密码,不会因为配置拉取失败就完全不能用。

4.4 提交逻辑与防重复提交

提交逻辑我在前面已经给过一版,这里补充防重复提交的细节。除了按钮的:loading之外,我还会在方法入口加一个判断:

async handleSubmit() { if (this.submitLoading) return try { await this.$refs.passwordForm.validate() } catch (e) { return } this.submitLoading = true try { await updatePassword({ oldPassword: this.form.oldPassword, newPassword: this.form.newPassword }) this.$message.success('密码修改成功,请重新登录') setTimeout(() => { localStorage.removeItem('token') this.$router.push('/login') }, 800) } finally { this.submitLoading = false } }

注意validate()失败时会抛出异常,所以要用 try-catch 包住,否则后面的代码不会执行。另外修改成功后延迟 800ms 再跳转,是为了让用户看到成功提示,直接跳转的话提示会一闪而过。

5. 常见问题与排查技巧实录

5.1 表单校验不触发或触发异常

这是最常见的问题,我整理了一个速查表:

现象可能原因解决办法
点击提交没有任何反应validate 抛出的异常没被 catch,或者 rules 里 prop 名和 model 字段名不一致检查 el-form-item 的 prop 是否与 form 字段完全对应
校验提示不消失字段值变化后没有触发重新校验在 input 的 change 事件里调用 validateField
自定义 validator 不执行validator 函数没有正确 callback确保每个分支都调用了 callback
确认密码校验滞后新密码变化没触发确认密码校验在新密码 change 时 validateField('confirmPassword')

5.2 全局异常拦截的边界情况

拦截器虽然好用,但有几个坑要注意。第一,文件上传接口的响应结构可能和普通接口不一样,如果全局拦截器统一按res.code判断,上传接口可能会误报。解决办法是在请求配置里加一个skipGlobalCheck标记,拦截器里判断这个标记决定是否走统一逻辑。

第二,拦截器里跳转路由要小心循环。比如 401 跳登录页,如果登录页本身的接口也返回 401,就会死循环。所以登录接口的请求要排除在拦截器之外,或者加一个isLoginRequest判断。

第三,Message 提示在拦截器里调用时,this 指向不是组件实例,所以不能用this.$message,要直接 import Message 组件。

5.3 密码修改成功后的处理策略

修改密码成功后,通常有两种处理方式:跳登录页让用户重新登录,或者留在原地提示成功。我的建议是一律跳登录页。原因是修改密码后,服务端通常会使旧 token 失效,如果留在原地,用户下一次操作就会遇到 401,体验反而更差。跳登录页虽然多一步操作,但逻辑上是清晰的。

跳转前记得清除本地存储的 token 和用户信息,避免残留数据导致登录页状态异常。

5.4 一些容易被忽略的细节

  • 密码输入框一定要加autocomplete="new-password",防止浏览器自动填充原密码到新密码框。
  • 如果项目支持多标签页,修改密码成功后要通知其他标签页登出,可以用localStorage的 storage 事件实现。
  • 密码强度校验的正则不要写得太复杂,否则用户很难满足,反而会降低体验。大小写字母加数字这个组合已经能覆盖大部分安全要求。
  • 接口超时时间不要设太短,修改密码涉及服务端加密和数据库写入,15 秒是比较合理的值。

6. 我个人在实际操作中的几点体会

做这类页面做多了之后,我越来越觉得前端开发的价值不在于把 UI 画得多漂亮,而在于把边界情况处理得多干净。修改密码这个页面,正常流程可能只占代码量的 30%,剩下 70% 都是在处理各种异常和边界。element UI 帮你解决了 UI 层面的问题,但状态管理、异常捕捉、用户体验这些,还是得自己一点点抠。

我现在的习惯是,每做一个表单页面,先列一张“异常清单”:用户可能怎么操作、接口可能怎么失败、网络可能怎么抖动,然后针对每一条设计反馈方案。这张清单列完,代码结构基本就清晰了。另外,全局异常拦截一定要在项目初期就搭好,不要等到页面做了一半才想起来加,那时候改造成本会高很多。

最后分享一个小技巧:修改密码页面的表单,可以在beforeRouteLeave里加一个判断,如果用户已经输入了内容但没提交,弹一个确认框问是否离开。这个功能看起来小,但能避免用户误操作丢失输入,实际用起来很加分。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询