- 新增 /profile 路由:两个 Tab(个人信息 / 修改密码)+ 退出按钮 - Sidebar 底部用户区改造:绑定真实登录用户(头像/姓名/角色)+ 加 Settings icon 跳 /profile - useAuthStore 加 refreshUser:保存信息后立即同步 Sidebar 显示 - 个人信息表单:姓名/手机/邮箱可改,部门/角色只读,复用 updateMember - 密码修改:原密 + 新密(≥8位且≠原密) + 确认 三档校验,眼睛 icon 切换可见,下次登录生效 - 退出登录:confirm → useAuthStore.logout → /login Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
181 lines
7.4 KiB
Markdown
181 lines
7.4 KiB
Markdown
# 个人中心管理设计
|
||
|
||
## Context
|
||
|
||
当前用户登录后无法编辑自己的个人信息和密码——这两件事现在只能由超管在"成员管理"里改。Sidebar 底部"Admin / 管理员"硬编码(与真实登录用户脱节)也是历史遗留 bug。
|
||
|
||
本期目标:新增 `/profile` 路由,让用户能改自己的姓名 / 手机 / 邮箱、修改密码、退出当前账号。顺手把 Sidebar 用户区改成绑定真实用户。
|
||
|
||
## 决策摘要
|
||
|
||
| 项 | 决策 |
|
||
|---|---|
|
||
| 入口 | Sidebar 底部用户区右侧加 `<Settings />` icon,仅 icon 跳转(整块不响应) |
|
||
| 可编辑字段 | 姓名 / 手机 / 邮箱(部门/角色只读展示) |
|
||
| 保存策略 | 复用 `useMemberStore.updateMember`;姓名/手机/邮箱保存后立刻 `useAuthStore.refreshUser` 同步 |
|
||
| 密码修改 | 原密 + 新密 + 确认 三档;新密 ≥ 8 字符且 ≠ 原密 |
|
||
| 密码生效时机 | 下次登录生效(不强制退出) |
|
||
| 退出登录 | 复用 `useAuthStore.logout` → 跳 `/login` |
|
||
| 路由权限 | 不加 RouteGuard(个人中心是当前用户自己的,无需权限点) |
|
||
| 默认 Tab | 「个人信息」 |
|
||
| 退出按钮位置 | Header 右上角,红色边框/红字 |
|
||
| 密码可见切换 | 末尾眼睛 icon(与 login 页风格一致) |
|
||
| Sidebar 副名 | 通过 `roleId` 反查 `roles` 拿角色名 |
|
||
| 部门展示 | 通过 `departmentId` 反查 `departments` 拿层级名(如"产品部 / 前端组") |
|
||
|
||
## 1. 入口(Sidebar 改造)
|
||
|
||
**`apps/web/components/layout/Sidebar.tsx`**
|
||
|
||
底部用户区当前硬编码"A / Admin / 管理员"。改造:
|
||
|
||
```
|
||
┌──────────────────────────────────────┐
|
||
│ [头像首字母] 张三 [⚙] │
|
||
│ 产品经理 │
|
||
└──────────────────────────────────────┘
|
||
```
|
||
|
||
- 头像首字母 = `user.name?.[0]`(fallback `'?'`)
|
||
- 主名 = `user.name`
|
||
- 副名 = `roles.find(r => r.id === user.roleId)?.name ?? '-'`
|
||
- 整块按钮**不响应点击**(移除 onClick)
|
||
- 右侧加 `<Settings className="h-4 w-4" />` 独立按钮 → `router.push('/profile')`
|
||
- 未登录:保留显示"未登录"占位,icon 不显示
|
||
|
||
## 2. 个人中心页
|
||
|
||
**新增 `apps/web/app/profile/page.tsx`**
|
||
|
||
```
|
||
┌─ Header (h-14) ──────────────────────────┐
|
||
│ 个人中心 [退出登录] │
|
||
├──────────────────────────────────────────┤
|
||
│ ┌─ 个人信息 ─┐ ┌─ 修改密码 ─┐ │ ← Tab 切换
|
||
├──────────────────────────────────────────┤
|
||
│ [Tab 内容区] │
|
||
└──────────────────────────────────────────┘
|
||
```
|
||
|
||
**入口校验**:`useAuthStore.user` 不存在则 `router.push('/login')`,避免直访问空 store。
|
||
|
||
**Tab 样式**:沿用版本详情页 `border-b-2 + accent` 色风格。
|
||
|
||
**退出登录按钮**:
|
||
- Header 右上角,红色边框红字白底
|
||
- `onClick`: `confirm('确认退出当前账号?')` → `useAuthStore.logout()` → `router.push('/login')`
|
||
|
||
## 3. 个人信息表单
|
||
|
||
```
|
||
姓名 * [_____________]
|
||
手机号 * [_____________] FieldError
|
||
邮箱 * [_____________] FieldError
|
||
部门 产品部 / 前端组 (只读,灰底)
|
||
角色 产品经理 (只读,灰底)
|
||
|
||
[取消] [保存]
|
||
```
|
||
|
||
**初始值**:从 `useMemberStore.members.find(m => m.id === user.id)` 取最新值(不直接信 `useAuthStore.user`)。
|
||
|
||
**校验**:
|
||
- 姓名:必填,trim 后非空,最长 20
|
||
- 手机号:必填,正则 `/^1[3-9]\d{9}$/`
|
||
- 邮箱:必填,正则 `/^[^\s@]+@[^\s@]+\.[^\s@]+$/`
|
||
- 失败:用 `<FieldError>` 在对应 input 下方显示文案
|
||
|
||
**部门展示**:递归查 `departmentId` 父子链,拼为"产品部 / 前端组"格式。
|
||
|
||
**角色展示**:`roles.find(r => r.id === user.roleId)?.name`。
|
||
|
||
**提交**:
|
||
1. 校验通过 → `useMemberStore.updateMember(user.id, { name, phone, email })`
|
||
2. 调 `useAuthStore.refreshUser()` 同步 user state(让 Sidebar 立即显示新名字)
|
||
3. 内联绿色提示"已保存"(2 秒自动消失)
|
||
|
||
**取消**:恢复初始值。
|
||
|
||
## 4. 密码修改表单 + 退出
|
||
|
||
```
|
||
当前密码 * [_____________ 👁] FieldError
|
||
新密码 * [_____________ 👁] FieldError
|
||
确认密码 * [_____________ 👁] FieldError
|
||
|
||
[取消] [保存]
|
||
```
|
||
|
||
**校验**:
|
||
- 当前密码:必填,等于 `member.password`,否则 `FieldError: '当前密码错误'`
|
||
- 新密码:必填,长度 ≥ 8(与 `passwordRule.length` 默认值一致),≠ 当前密码
|
||
- 确认密码:必填,必须等于新密码
|
||
|
||
**提交**:
|
||
1. 三项校验通过 → `useMemberStore.updateMember(user.id, { password: newPassword })`
|
||
2. 清空三个输入框
|
||
3. 内联绿色提示"密码已修改,下次登录生效"
|
||
4. 不强制退出
|
||
|
||
**眼睛 icon**:每个 input 末尾切换 `type="password" / "text"`,复用 login 页 Eye/EyeOff 模式。
|
||
|
||
**取消**:清空三个输入框。
|
||
|
||
## 数据流
|
||
|
||
```
|
||
useAuthStore.user (id, name, roleId, departmentId, phone, email)
|
||
│
|
||
├─ Sidebar 用户区 ─→ 头像/姓名 + 角色名 + 设置 icon
|
||
│
|
||
└─ /profile 页面入口校验
|
||
│
|
||
├─ 个人信息表单 ─→ updateMember() ─→ refreshUser() ─→ Sidebar 立即更新
|
||
├─ 密码表单 ─→ updateMember() ─→ 提示下次登录生效
|
||
└─ 退出按钮 ─→ logout() ─→ router.push('/login')
|
||
```
|
||
|
||
## useAuthStore 改造
|
||
|
||
新增 `refreshUser()` 方法:
|
||
|
||
```ts
|
||
refreshUser: () => {
|
||
const user = get().user;
|
||
if (!user) return;
|
||
// 从 localStorage 重新读 member(与 login 同源)
|
||
const membersRaw = localStorage.getItem('ftb_members_v1');
|
||
if (!membersRaw) return;
|
||
const parsed = JSON.parse(membersRaw);
|
||
const m = parsed.members?.find((x: any) => x.id === user.id);
|
||
if (!m) return;
|
||
const next = { id: m.id, name: m.name, roleId: m.roleId, departmentId: m.departmentId, phone: m.phone, email: m.email };
|
||
set({ user: next });
|
||
sessionStorage.setItem('ftb_auth_session', JSON.stringify(next));
|
||
if (localStorage.getItem('ftb_auth_persist')) {
|
||
localStorage.setItem('ftb_auth_persist', JSON.stringify(next));
|
||
}
|
||
}
|
||
```
|
||
|
||
## 实施清单
|
||
|
||
新增:
|
||
- `apps/web/app/profile/page.tsx` — 个人中心页面(含两个 Tab + 退出按钮)
|
||
|
||
修改:
|
||
- `apps/web/components/layout/Sidebar.tsx` — 用户区绑定真实 user + 加 Settings icon
|
||
- `apps/web/stores/useAuthStore.ts` — 加 `refreshUser` 方法
|
||
|
||
## 验证
|
||
|
||
1. `pnpm type-check` 0 错误
|
||
2. `pnpm build` 14+1 个页面成功(新增 `/profile`)
|
||
3. 登录张三(产品经理):
|
||
- Sidebar 底部显示"张 张三 / 产品经理 / ⚙"
|
||
- 点 ⚙ 跳 `/profile`
|
||
- 改姓名提交后 Sidebar 立即同步新名字
|
||
- 改密码后提示下次登录生效,重新登录用新密码可登入旧密码失败
|
||
- 点退出 → 弹确认 → 跳 /login + session 清空
|
||
4. 三个表单的校验:手机/邮箱格式错误、新密短于 8、新密=旧密、确认不一致——都能在对应字段下显示红色文案
|