docs(plan): 切片 1 按钮级权限控制实施计划
Some checks failed
CI / rust-check (push) Has been cancelled
CI / rust-test (push) Has been cancelled
CI / frontend-build (push) Has been cancelled
CI / security-audit (push) Has been cancelled

18 个 Task,2 个 Chunk:
- Chunk 1: 权限基础设施(JWT 解码 + store + hook + 组件)
- Chunk 2: 健康模块 15 页面按钮改造 + 集成验证
This commit is contained in:
iven
2026-04-25 22:31:49 +08:00
parent 46089adbc6
commit a704ad7606

View File

@@ -0,0 +1,657 @@
# 切片 1: 按钮级权限控制 实施计划
> **For agentic workers:** REQUIRED: Use superpowers:subagent-driven-development (if subagents available) or superpowers:executing-plans to implement this plan. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 实现前端按钮级权限控制让无权限用户看不到操作按钮hidden 模式)。
**Architecture:** JWT claims 已包含 `permissions: Vec<String>`(后端登录时写入),前端复用 `client.ts``decodeJwtPayload` 提取权限码列表,存入 Zustand auth store。新增 `usePermission` hook + `AuthButton` / `AuthGuard` 声明式组件,包裹健康模块 15 个页面的操作按钮。
**Tech Stack:** React 19 + TypeScript + Zustand 5 + Ant Design 6
**设计规格:** `docs/superpowers/specs/2026-04-25-feature-completion-design.md` §2
---
## Chunk 1: 权限基础设施
### Task 1: 从 JWT 提取 permissions 并存入 auth store
**Files:**
- Modify: `apps/web/src/stores/auth.ts`
**背景:** JWT payload 已包含 `permissions` 字段string 数组)。`client.ts` 已有 `decodeJwtPayload` 函数。auth store 登录时已存 `access_token` 到 localStorage可从中解码权限。
- [ ] **Step 1: 在 auth store 中添加 permissions 状态和提取逻辑**
`apps/web/src/stores/auth.ts` 中:
1. 新增辅助函数文件顶部import 之后):
```typescript
function extractPermissions(): string[] {
const token = localStorage.getItem('access_token');
if (!token) return [];
try {
const parts = token.split('.');
if (parts.length !== 3) return [];
const payload = JSON.parse(atob(parts[1].replace(/-/g, '+').replace(/_/g, '/')));
return Array.isArray(payload.permissions) ? payload.permissions : [];
} catch {
return [];
}
}
```
2. 修改 `restoreInitialState` 返回值,增加 `permissions`:
```typescript
function restoreInitialState(): { user: UserInfo | null; isAuthenticated: boolean; permissions: string[] } {
const token = localStorage.getItem('access_token');
const userStr = localStorage.getItem('user');
if (token && userStr) {
try {
const user = JSON.parse(userStr) as UserInfo;
return { user, isAuthenticated: true, permissions: extractPermissions() };
} catch {
localStorage.removeItem('user');
}
}
return { user: null, isAuthenticated: false, permissions: [] };
}
```
3. 修改 `AuthState` 接口,增加 `permissions`:
```typescript
interface AuthState {
user: UserInfo | null;
isAuthenticated: boolean;
loading: boolean;
permissions: string[];
login: (username: string, password: string) => Promise<void>;
logout: () => Promise<void>;
loadFromStorage: () => void;
}
```
4. 修改 store 创建,初始化 `permissions`,在 login/logout 中同步更新:
```typescript
export const useAuthStore = create<AuthState>((set) => ({
user: initial.user,
isAuthenticated: initial.isAuthenticated,
loading: false,
permissions: initial.permissions,
login: async (username, password) => {
set({ loading: true });
try {
const resp = await apiLogin({ username, password });
localStorage.setItem('access_token', resp.access_token);
localStorage.setItem('refresh_token', resp.refresh_token);
localStorage.setItem('user', JSON.stringify(resp.user));
set({ user: resp.user, isAuthenticated: true, loading: false, permissions: extractPermissions() });
} catch (error) {
set({ loading: false });
throw error;
}
},
logout: async () => {
try {
await apiLogout();
} catch {
// Ignore logout API errors
}
localStorage.removeItem('access_token');
localStorage.removeItem('refresh_token');
localStorage.removeItem('user');
set({ user: null, isAuthenticated: false, permissions: [] });
},
loadFromStorage: () => {
const state = restoreInitialState();
set({ user: state.user, isAuthenticated: state.isAuthenticated, permissions: state.permissions });
},
}));
```
- [ ] **Step 2: 验证编译通过**
Run: `cd apps/web && npx tsc --noEmit`
Expected: 无类型错误
- [ ] **Step 3: 提交**
```bash
git add apps/web/src/stores/auth.ts
git commit -m "feat(web): auth store 添加 permissions 状态,从 JWT 解码提取"
```
---
### Task 2: 创建 usePermission hook
**Files:**
- Create: `apps/web/src/hooks/usePermission.ts`
- [ ] **Step 1: 创建 usePermission hook**
```typescript
import { useAuthStore } from '../stores/auth';
export function usePermission(code: string): { hasPermission: boolean } {
const permissions = useAuthStore((s) => s.permissions);
return { hasPermission: permissions.includes(code) };
}
```
- [ ] **Step 2: 验证编译通过**
Run: `cd apps/web && npx tsc --noEmit`
Expected: 无类型错误
- [ ] **Step 3: 提交**
```bash
git add apps/web/src/hooks/usePermission.ts
git commit -m "feat(web): 添加 usePermission hook"
```
---
### Task 3: 创建 AuthButton + AuthGuard 组件
**Files:**
- Create: `apps/web/src/components/AuthButton.tsx`
- Create: `apps/web/src/components/AuthGuard.tsx`
- [ ] **Step 1: 创建 AuthButton 组件**
`apps/web/src/components/AuthButton.tsx`:
```typescript
import type { ReactNode } from 'react';
import { usePermission } from '../hooks/usePermission';
interface AuthButtonProps {
code: string;
children: ReactNode;
}
export function AuthButton({ code, children }: AuthButtonProps) {
const { hasPermission } = usePermission(code);
if (!hasPermission) return null;
return <>{children}</>;
}
```
- [ ] **Step 2: 创建 AuthGuard 组件**
`apps/web/src/components/AuthGuard.tsx`:
```typescript
import type { ReactNode } from 'react';
import { usePermission } from '../hooks/usePermission';
interface AuthGuardProps {
code: string;
children: ReactNode;
}
export function AuthGuard({ code, children }: AuthGuardProps) {
const { hasPermission } = usePermission(code);
if (!hasPermission) return null;
return <>{children}</>;
}
```
- [ ] **Step 3: 验证编译通过**
Run: `cd apps/web && npx tsc --noEmit`
Expected: 无类型错误
- [ ] **Step 4: 提交**
```bash
git add apps/web/src/components/AuthButton.tsx apps/web/src/components/AuthGuard.tsx
git commit -m "feat(web): 添加 AuthButton/AuthGuard 声明式权限组件"
```
---
## Chunk 2: 健康模块页面按钮权限改造
### Task 4: PatientList 按钮权限
**Files:**
- Modify: `apps/web/src/pages/health/PatientList.tsx`
**改造目标:**
- 第 304 行 `新建患者` 按钮 → `<AuthButton code="health.patient.manage">`
- 第 242-267 行 操作列的编辑/删除按钮 → `<AuthButton code="health.patient.manage">`
- [ ] **Step 1: 添加 import**
在 PatientList.tsx 顶部 import 区域添加:
```typescript
import { AuthButton } from '../../components/AuthButton';
```
- [ ] **Step 2: 包裹新建患者按钮**
将第 304 行:
```tsx
<Button type="primary" icon={<PlusOutlined />} onClick={openCreateModal}>
</Button>
```
改为:
```tsx
<AuthButton code="health.patient.manage">
<Button type="primary" icon={<PlusOutlined />} onClick={openCreateModal}>
</Button>
</AuthButton>
```
- [ ] **Step 3: 包裹操作列按钮**
将 columns 操作列的 render第 241-270 行):
```tsx
render: (_: unknown, record: PatientListItem) => (
<Space size={4}>
<Button ... />
<Popconfirm ...><Button ... /></Popconfirm>
</Space>
),
```
改为:
```tsx
render: (_: unknown, record: PatientListItem) => (
<AuthButton code="health.patient.manage">
<Space size={4}>
<Button
size="small"
type="text"
icon={<EditOutlined />}
onClick={(e) => {
e.stopPropagation();
openEditModal(record);
}}
style={{ color: isDark ? '#94a3b8' : '#475569' }}
/>
<Popconfirm
title="确定删除此患者?"
onConfirm={(e) => {
e?.stopPropagation();
handleDelete(record.id);
}}
>
<Button
size="small"
type="text"
icon={<DeleteOutlined />}
danger
onClick={(e) => e.stopPropagation()}
/>
</Popconfirm>
</Space>
</AuthButton>
),
```
- [ ] **Step 4: 验证编译通过**
Run: `cd apps/web && npx tsc --noEmit`
Expected: 无类型错误
- [ ] **Step 5: 提交**
```bash
git add apps/web/src/pages/health/PatientList.tsx
git commit -m "feat(web): PatientList 添加按钮级权限控制"
```
---
### Task 5: AppointmentList 按钮权限
**Files:**
- Modify: `apps/web/src/pages/health/AppointmentList.tsx`
**改造模式同 Task 4**
- 新建预约按钮 → `<AuthButton code="health.appointment.manage">`
- 操作列(编辑/取消/状态变更) → `<AuthButton code="health.appointment.manage">`
- [ ] **Step 1: 读取文件,识别所有操作按钮位置**
Run: `grep -n "Button\|onClick\|Popconfirm" apps/web/src/pages/health/AppointmentList.tsx`
- [ ] **Step 2: 添加 import + 包裹所有操作按钮**
添加 `import { AuthButton } from '../../components/AuthButton';`
`<AuthButton code="health.appointment.manage">` 包裹:
- 顶部新建按钮
- 表格操作列中的所有按钮
- [ ] **Step 3: 验证编译通过**
Run: `cd apps/web && npx tsc --noEmit`
- [ ] **Step 4: 提交**
```bash
git add apps/web/src/pages/health/AppointmentList.tsx
git commit -m "feat(web): AppointmentList 添加按钮级权限控制"
```
---
### Task 6: DoctorList 按钮权限
**Files:**
- Modify: `apps/web/src/pages/health/DoctorList.tsx`
**权限码:** `health.doctor.manage`
- [ ] **Step 1: 添加 import + 包裹操作按钮**
模式同 Task 4-5。新建按钮 + 操作列用 `<AuthButton code="health.doctor.manage">` 包裹。
- [ ] **Step 2: 验证 + 提交**
```bash
cd apps/web && npx tsc --noEmit
git add apps/web/src/pages/health/DoctorList.tsx
git commit -m "feat(web): DoctorList 添加按钮级权限控制"
```
---
### Task 7: DoctorSchedule 按钮权限
**Files:**
- Modify: `apps/web/src/pages/health/DoctorSchedule.tsx`
**权限码:** `health.doctor.manage`
- [ ] **Step 1: 添加 import + 包裹操作按钮**
新建排班 + 操作列用 `<AuthButton code="health.doctor.manage">` 包裹。
- [ ] **Step 2: 验证 + 提交**
```bash
cd apps/web && npx tsc --noEmit
git add apps/web/src/pages/health/DoctorSchedule.tsx
git commit -m "feat(web): DoctorSchedule 添加按钮级权限控制"
```
---
### Task 8: FollowUpTaskList 按钮权限
**Files:**
- Modify: `apps/web/src/pages/health/FollowUpTaskList.tsx`
**权限码:** `health.follow-up.manage`
- [ ] **Step 1: 添加 import + 包裹操作按钮**
新建随访 + 操作列(编辑/完成/取消)用 `<AuthButton code="health.follow-up.manage">` 包裹。
- [ ] **Step 2: 验证 + 提交**
```bash
cd apps/web && npx tsc --noEmit
git add apps/web/src/pages/health/FollowUpTaskList.tsx
git commit -m "feat(web): FollowUpTaskList 添加按钮级权限控制"
```
---
### Task 9: FollowUpRecordList 按钮权限
**Files:**
- Modify: `apps/web/src/pages/health/FollowUpRecordList.tsx`
**权限码:** `health.follow-up.manage`
- [ ] **Step 1: 添加 import + 包裹操作按钮**
添加记录 + 操作列用 `<AuthButton code="health.follow-up.manage">` 包裹。
- [ ] **Step 2: 验证 + 提交**
```bash
cd apps/web && npx tsc --noEmit
git add apps/web/src/pages/health/FollowUpRecordList.tsx
git commit -m "feat(web): FollowUpRecordList 添加按钮级权限控制"
```
---
### Task 10: ConsultationList 按钮权限
**Files:**
- Modify: `apps/web/src/pages/health/ConsultationList.tsx`
**权限码:** `health.consultation.manage`
- [ ] **Step 1: 添加 import + 包裹操作按钮**
新建会话 + 操作列(关闭/导出)用 `<AuthButton code="health.consultation.manage">` 包裹。
- [ ] **Step 2: 验证 + 提交**
```bash
cd apps/web && npx tsc --noEmit
git add apps/web/src/pages/health/ConsultationList.tsx
git commit -m "feat(web): ConsultationList 添加按钮级权限控制"
```
---
### Task 11: ConsultationDetail 按钮权限
**Files:**
- Modify: `apps/web/src/pages/health/ConsultationDetail.tsx`
**权限码:** `health.consultation.manage`
- [ ] **Step 1: 添加 import + 包裹操作按钮**
发送消息 + 关闭会话 + 导出按钮用 `<AuthButton code="health.consultation.manage">` 包裹。
- [ ] **Step 2: 验证 + 提交**
```bash
cd apps/web && npx tsc --noEmit
git add apps/web/src/pages/health/ConsultationDetail.tsx
git commit -m "feat(web): ConsultationDetail 添加按钮级权限控制"
```
---
### Task 12: OfflineEventList 按钮权限
**Files:**
- Modify: `apps/web/src/pages/health/OfflineEventList.tsx`
**权限码:** `health.articles.manage`
- [ ] **Step 1: 添加 import + 包裹操作按钮**
新建活动 + 操作列用 `<AuthButton code="health.articles.manage">` 包裹。
- [ ] **Step 2: 验证 + 提交**
```bash
cd apps/web && npx tsc --noEmit
git add apps/web/src/pages/health/OfflineEventList.tsx
git commit -m "feat(web): OfflineEventList 添加按钮级权限控制"
```
---
### Task 13: PatientDetail 按钮权限
**Files:**
- Modify: `apps/web/src/pages/health/PatientDetail.tsx`
**权限码:** `health.patient.manage`
- [ ] **Step 1: 添加 import + 包裹操作按钮**
编辑患者信息按钮 + 标签管理 + 新增健康数据按钮用 `<AuthButton code="health.patient.manage">` 包裹。
- [ ] **Step 2: 验证 + 提交**
```bash
cd apps/web && npx tsc --noEmit
git add apps/web/src/pages/health/PatientDetail.tsx
git commit -m "feat(web): PatientDetail 添加按钮级权限控制"
```
---
### Task 14: PatientTagManage 按钮权限
**Files:**
- Modify: `apps/web/src/pages/health/PatientTagManage.tsx`
**权限码:** `health.patient.manage`
- [ ] **Step 1: 添加 import + 包裹操作按钮**
新建标签 + 编辑/删除标签用 `<AuthButton code="health.patient.manage">` 包裹。
- [ ] **Step 2: 验证 + 提交**
```bash
cd apps/web && npx tsc --noEmit
git add apps/web/src/pages/health/PatientTagManage.tsx
git commit -m "feat(web): PatientTagManage 添加按钮级权限控制"
```
---
### Task 15: PointsProductList 按钮权限
**Files:**
- Modify: `apps/web/src/pages/health/PointsProductList.tsx`
**权限码:** `health.points.manage`
- [ ] **Step 1: 添加 import + 包裹操作按钮**
新建商品 + 编辑/删除/上下架用 `<AuthButton code="health.points.manage">` 包裹。
- [ ] **Step 2: 验证 + 提交**
```bash
cd apps/web && npx tsc --noEmit
git add apps/web/src/pages/health/PointsProductList.tsx
git commit -m "feat(web): PointsProductList 添加按钮级权限控制"
```
---
### Task 16: PointsOrderList 按钮权限
**Files:**
- Modify: `apps/web/src/pages/health/PointsOrderList.tsx`
**权限码:** `health.points.list`(只读列表,如有核销操作用 `health.points.manage`
- [ ] **Step 1: 读取文件,识别是否有写操作按钮**
Run: `grep -n "Button\|onClick" apps/web/src/pages/health/PointsOrderList.tsx`
- [ ] **Step 2: 如有核销/管理按钮,用 `<AuthButton code="health.points.manage">` 包裹**
- [ ] **Step 3: 验证 + 提交**
```bash
cd apps/web && npx tsc --noEmit
git add apps/web/src/pages/health/PointsOrderList.tsx
git commit -m "feat(web): PointsOrderList 添加按钮级权限控制"
```
---
### Task 17: PointsRuleList 按钮权限
**Files:**
- Modify: `apps/web/src/pages/health/PointsRuleList.tsx`
**权限码:** `health.points.manage`
- [ ] **Step 1: 添加 import + 包裹操作按钮**
新建规则 + 编辑/删除用 `<AuthButton code="health.points.manage">` 包裹。
- [ ] **Step 2: 验证 + 提交**
```bash
cd apps/web && npx tsc --noEmit
git add apps/web/src/pages/health/PointsRuleList.tsx
git commit -m "feat(web): PointsRuleList 添加按钮级权限控制"
```
---
### Task 18: 集成验证
- [ ] **Step 1: 全量 TypeScript 编译检查**
Run: `cd apps/web && npx tsc --noEmit`
Expected: 0 errors
- [ ] **Step 2: 启动前端开发服务器**
Run: `cd apps/web && pnpm dev`
- [ ] **Step 3: 功能验证**
1. 用管理员账号登录 → 所有按钮可见
2. 创建一个无权限的测试角色(仅 `health.patient.list`)→ 分配给测试用户
3. 用测试用户登录 → 仅患者列表可见,新建/编辑/删除按钮隐藏
4. 确认表格行点击导航(如患者详情页)仍然正常
- [ ] **Step 4: 生产构建验证**
Run: `cd apps/web && pnpm build`
Expected: 构建成功
- [ ] **Step 5: 推送所有提交**
```bash
git push
```
---
## 权限码速查表
| 页面 | 文件 | 权限码 |
|------|------|--------|
| PatientList | PatientList.tsx | health.patient.manage |
| PatientDetail | PatientDetail.tsx | health.patient.manage |
| PatientTagManage | PatientTagManage.tsx | health.patient.manage |
| AppointmentList | AppointmentList.tsx | health.appointment.manage |
| DoctorList | DoctorList.tsx | health.doctor.manage |
| DoctorSchedule | DoctorSchedule.tsx | health.doctor.manage |
| FollowUpTaskList | FollowUpTaskList.tsx | health.follow-up.manage |
| FollowUpRecordList | FollowUpRecordList.tsx | health.follow-up.manage |
| ConsultationList | ConsultationList.tsx | health.consultation.manage |
| ConsultationDetail | ConsultationDetail.tsx | health.consultation.manage |
| OfflineEventList | OfflineEventList.tsx | health.articles.manage |
| PointsProductList | PointsProductList.tsx | health.points.manage |
| PointsOrderList | PointsOrderList.tsx | health.points.manage |
| PointsRuleList | PointsRuleList.tsx | health.points.manage |
| StatisticsDashboard | StatisticsDashboard.tsx | health.health-data.list (只读,无操作按钮) |