234 lines
7.9 KiB
Markdown
234 lines
7.9 KiB
Markdown
# 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
|
||
|
||
```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 <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 个文件改动。
|