# 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` 变更要点 - 在分隔线与「退出登录」之间新增 `
  • ` - 图标:`ArtSvgIcon icon="ri:lock-password-line"` - 文案:`修改密码`(或 `$t('topBar.user.changePassword')`) - 根节点包裹 `` - `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 ```typescript /** 当前用户修改密码 */ 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()` 逻辑 ```python 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 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 个文件改动。