提交
This commit is contained in:
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 个文件改动。
|
||||
Reference in new issue
Block a user