This commit is contained in:
zlwu committed 2026-07-12 18:23:02 +08:00
1 parent 2142b33cfa
commit 3520f9ce90
27 files changed
+248553 -252041

No files matched your search

@@ -0,0 +1,60 @@
# web-admin 顶栏修改密码 Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 在顶栏头像下拉「退出登录」上方增加「修改密码」,用户自助改密后当前会话继续有效。
**Architecture:** 前端 `ArtUserMenu` 增加菜单项并打开 `UserPasswordDialog`;弹窗调用 `fetchChangePassword`;后端 `public.py` 新增 `POST /web/auth/change-password`,CRUD 层 `change_password()` 校验原密码并 bcrypt 更新。
**Tech Stack:** Vue 3 + Element Plus(web-admin);FastAPI + SQLAlchemy + bcrypt(backend)
**Spec:** `docs/superpowers/specs/2026-07-12-web-admin-change-password-design.md`
---
### Task 1: 后端 CRUD `change_password`
**Files:**
- Modify: `backend/app/crud/user.py`
- [x] **Step 1:** 在 `reset_password` 上方新增 `change_password(db, user_id, old_password, new_password)`
- [x] **Step 2:** 校验用户存在、原密码 bcrypt、新密码 ≥6 位、新≠旧
### Task 2: 后端路由 `POST /web/auth/change-password`
**Files:**
- Modify: `backend/app/api/public.py`
- [x] **Step 1:** 从 Bearer Token 解析当前用户
- [x] **Step 2:** 读取 `oldPassword`/`newPassword`(兼容蛇形)
- [x] **Step 3:** 调用 `change_password`,返回成功/400/401
### Task 3: 前端 API
**Files:**
- Modify: `web-admin/src/api/auth.ts`
- [x] **Step 1:** 新增 `fetchChangePassword({ oldPassword, newPassword })`
### Task 4: 前端弹窗 `UserPasswordDialog.vue`
**Files:**
- Create: `web-admin/src/components/core/layouts/art-header-bar/widget/UserPasswordDialog.vue`
- [x] **Step 1:** 三字段表单弹窗(原密码/新密码/确认)
- [x] **Step 2:** Fresh Mint 弹窗规范 + Close/Check 底栏
### Task 5: 顶栏菜单入口
**Files:**
- Modify: `web-admin/src/components/core/layouts/art-header-bar/widget/ArtUserMenu.vue`
- Modify: `web-admin/src/locales/langs/zh.json`
- Modify: `web-admin/src/locales/langs/en.json`
- [x] **Step 1:** 退出登录上方增加「修改密码」菜单项
- [x] **Step 2:** 引入 `UserPasswordDialog`,点击打开
### Task 6: 验证
- [x] **Step 1:** 后端语法检查 `python -m py_compile`
- [x] **Step 2:** 前端 lint 检查改动文件
@@ -0,0 +1,233 @@
# 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 个文件改动。