7.9 KiB
7.9 KiB
web-admin 顶栏用户菜单 — 修改密码设计
日期: 2026-07-12
状态: 待实施
范围:web-admin顶栏头像下拉 +backend当前用户改密接口
UI 规范:docs/web-admin-ui-spec-fresh-mint.md
参考实现: 并列仓库zkxw(ArtUserMenu.vue+UserPasswordDialog.vue+/web/auth/change-password)
背景与目标
运营人员在 web-admin 顶栏点击头像后,下拉菜单目前仅有电话/区域展示与「退出登录」,无法自助修改密码,只能联系管理员在用户管理页重置为默认密码 111111。
目标:在头像下拉菜单中,于「退出登录」按钮上方增加「修改密码」入口,用户填写原密码与新密码后完成自助改密。
已确认决策
| # | 决策 | 说明 |
|---|---|---|
| D1 | 交互形态 | 方案 A:菜单项 + 独立弹窗 UserPasswordDialog(对齐 zkxw) |
| D2 | 改密后会话 | 不强制重新登录;改密成功后提示并关闭弹窗,当前 Token 会话继续有效 |
| D3 | 权限 | 仅需登录态(Bearer Token),不新增菜单权限码 |
| D4 | 密码规则 | 新密码最少 6 位;校验原密码 bcrypt;确认密码前端一致性校验 |
| D5 | 传输安全 | 走 HTTPS + 现有 API 加密中间件(生产环境);不单独对改密请求做 RSA(与登录区分,同 zkxw) |
| D6 | 范围 | 仅「修改密码」;本期不做「个人信息」编辑 |
非目标(本期不做)
- 忘记密码 / 邮件找回
- 密码复杂度策略(大小写+数字+特殊字符)
- 密码历史表 / 防重复使用旧密码
- 首次登录强制改密
- 改密后吊销当前会话 / 强制退出
- 小程序端改密
一、现状与缺口
前端
| 项 | 状态 |
|---|---|
ArtUserMenu.vue 菜单入口 |
❌ 无「修改密码」 |
UserPasswordDialog.vue |
❌ 不存在 |
fetchChangePassword() |
❌ api/auth.ts 未定义 |
入口文件:web-admin/src/components/core/layouts/art-header-bar/widget/ArtUserMenu.vue
由 art-header-bar/index.vue 引用。
后端
| 项 | 状态 |
|---|---|
ChangePasswordRequest Schema |
✅ app/schemas/auth.py(已有,未接线) |
change_password() CRUD |
❌ 仅有 reset_password()(管理员重置) |
POST /web/auth/change-password |
❌ 未实现 |
已有管理员重置:POST /web/admin/users/{id}/reset-password → 固定重置为 111111。
二、目标交互
菜单布局
┌─────────────────────┐
│ [头像] 张三 │
│ 运营管理员 │
├─────────────────────┤
│ 👤 138xxxx │
│ 📍 华东区(若有) │
├─────────────────────┤
│ 🔒 修改密码 ← 新增 │
├─────────────────────┤
│ [ 退出登录 ] │
└─────────────────────┘
流程
- 用户悬停头像 → 点击「修改密码」
ArtUserMenu关闭 Popover(沿用closeUserMenu()+setTimeout模式)- 打开
UserPasswordDialog(v-model:visible) - 用户填写:原密码、新密码、确认新密码
- 前端校验通过后
POST /web/auth/change-password - 成功 → 全局成功提示、关闭弹窗、不调用
logOut() - 失败 → 展示后端错误(如「原密码不正确」)
三、前端设计
改动文件
| 文件 | 操作 |
|---|---|
web-admin/src/components/core/layouts/art-header-bar/widget/ArtUserMenu.vue |
修改:增加菜单项 + 引入弹窗 |
web-admin/src/components/core/layouts/art-header-bar/widget/UserPasswordDialog.vue |
新建:改密弹窗 |
web-admin/src/api/auth.ts |
修改:新增 fetchChangePassword() |
web-admin/src/locales/langs/zh.json |
可选:topBar.user.changePassword |
web-admin/src/locales/langs/en.json |
可选:同上英文 |
ArtUserMenu.vue 变更要点
- 在分隔线与「退出登录」之间新增
<li class="btn-item" @click="openPasswordDialog"> - 图标:
ArtSvgIcon icon="ri:lock-password-line" - 文案:
修改密码(或$t('topBar.user.changePassword')) - 根节点包裹
<UserPasswordDialog v-model:visible="passwordDialogVisible" /> openPasswordDialog()模式对齐 zkxw:closeUserMenu()→setTimeout 150ms→passwordDialogVisible = true
UserPasswordDialog.vue 规范
对齐 Fresh Mint 弹窗约定(web-admin-crud-dialog.mdc):
el-dialog:class="dialog-form-layout"、align-center、append-to-body、modal-class="protocol-dialog-mask"、destroy-on-close、:close-on-click-modal="false"el-form:label-position="top"、scroll-to-error- 字段(均
form-col-span-2):- 原密码:
type="password"、show-password、autocomplete="current-password" - 新密码:
min 6校验、autocomplete="new-password" - 确认新密码:自定义 validator 与
newPassword一致
- 原密码:
- Footer:取消(
Close图标)、确定(Check图标 +:loading="submitting") - 打开时
resetForm()清空字段与校验态
API
/** 当前用户修改密码 */
export function fetchChangePassword(data: { oldPassword: string; newPassword: string }) {
return request.post({
url: '/web/auth/change-password',
data,
showSuccessMessage: true
})
}
四、后端设计
改动文件
| 文件 | 操作 |
|---|---|
backend/app/crud/user.py |
新增 change_password() |
backend/app/api/public.py |
新增 POST /web/auth/change-password |
change_password() 逻辑
def change_password(db: Session, user_id: str, old_password: str, new_password: str) -> None:
obj = get_user(db, user_id)
if not obj:
raise ValueError("用户不存在")
if not verify_password(old_password, obj.password or ""):
raise ValueError("原密码不正确")
new_pw = (new_password or "").strip()
if len(new_pw) < 6:
raise ValueError("新密码长度不能少于6位")
if old_password == new_pw:
raise ValueError("新密码不能与原密码相同")
obj.password = _hash_password(new_pw)
obj.update_time = datetime.now()
db.add(obj)
db.commit()
路由
POST /web/auth/change-password
Authorization: Bearer <access_token>
Content-Type: application/json
Request body(驼峰,与前端一致):
{
"oldPassword": "string",
"newPassword": "string"
}
Response 200:
{ "code": 200, "data": { "message": "密码修改成功" } }
Response 400:
- "请输入原密码"
- "新密码长度不能少于6位"
- "原密码不正确"
- "新密码不能与原密码相同"
Response 401:
- "Token无效或已过期" / "未授权"
鉴权:从 Authorization Bearer Token 解析当前用户 user_id(复用 get_current_user_by_token 或 public.py 现有模式)。
五、校验规则汇总
| 规则 | 前端 | 后端 |
|---|---|---|
| 原密码必填 | ✅ | ✅ |
| 新密码 ≥ 6 位 | ✅ | ✅ |
| 确认密码一致 | ✅ | — |
| 原密码 bcrypt 校验 | — | ✅ |
| 新密码 ≠ 原密码 | 建议 | ✅ |
六、验收清单
- 头像下拉在「退出登录」上方显示「修改密码」
- 点击后 Popover 关闭,弹窗居中打开
- 原密码错误 → 「原密码不正确」
- 新密码 < 6 位 → 前端拦截
- 两次新密码不一致 → 前端拦截
- 改密成功 → 成功提示,弹窗关闭,不退出登录
- 登出后用新密码可重新登录
- 弹窗符合 Fresh Mint(
dialog-form-layout、无 backdrop blur、按钮带图标)
七、工作量
| 模块 | 预估 |
|---|---|
| 前端 | ~1h |
| 后端 | ~0.5h |
| 联调 | ~0.5h |
| 合计 | ~2h |
无数据库迁移;约 6 个文件改动。