Files
hms/docs/archive/superpowers-completed/2026-04-25-slice1-button-permissions.md
iven 18fa6ce6d4 docs: 全局文档梳理归档 — 删除过期文件 + 归档 V1/早期设计 + wiki 数据校正 + CLAUDE.md 规则优化
**根目录清理:**
- 删除 CLAUDE-1.md(ZCLAW 旧项目配置,HMS 已完全脱离)
- 移动 DESIGN.md → docs/archive/(ERP 旧设计系统)
- 删除 plans/ 98 个临时会话计划文件

**归档重组:**
- V1 审计(12 文件)→ docs/archive/audits-v1/
- 早期 CRM/插件迭代设计(13 文件)→ docs/archive/superpowers-early/
- 已完成/已取代设计(28 文件)→ docs/archive/superpowers-completed/
- 早期讨论/测试报告 → docs/archive/discussions-early/ + test-reports-early/
- QA 重复文件清理(3 个旧版 result 文件)

**wiki 数据校正:**
- 迁移数 137→145,源文件 599→649,提交数 720→800+
- 小程序文件 124→163,Web 前端 297→332
- 后端测试 999→943(实际统计),权限码 75+→128
- 文档索引新增归档目录说明

**CLAUDE.md 规则优化:**
- §2.5 闭环工作法:提交+文档+推送三合一 + wiki 更新触发条件
- §2.6 Feature DoD:新增文档一致性检查项
- §6 反模式:新增 wiki 更新滞后/推送不及时警告
2026-05-15 09:29:04 +08:00

658 lines
17 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 切片 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 (只读,无操作按钮) |