Files
shanxia2.0/docs/superpowers/specs/2026-07-12-web-admin-change-password-design.md
T
2026-07-12 18:23:02 +08:00

7.9 KiB
Raw Blame History

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          │
│ 📍 华东区(若有)    │
├─────────────────────┤
│ 🔒 修改密码    ← 新增 │
├─────────────────────┤
│     [ 退出登录 ]     │
└─────────────────────┘

流程

  1. 用户悬停头像 → 点击「修改密码」
  2. ArtUserMenu 关闭 Popover(沿用 closeUserMenu() + setTimeout 模式)
  3. 打开 UserPasswordDialog(v-model:visible)
  4. 用户填写:原密码、新密码、确认新密码
  5. 前端校验通过后 POST /web/auth/change-password
  6. 成功 → 全局成功提示、关闭弹窗、不调用 logOut()
  7. 失败 → 展示后端错误(如「原密码不正确」)

三、前端设计

改动文件

文件 操作
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 个文件改动。