Files
hms/docs/superpowers/plans/2026-04-25-slice1-button-permissions.md
iven a704ad7606
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
docs(plan): 切片 1 按钮级权限控制实施计划
18 个 Task,2 个 Chunk:
- Chunk 1: 权限基础设施(JWT 解码 + store + hook + 组件)
- Chunk 2: 健康模块 15 页面按钮改造 + 集成验证
2026-04-25 22:31:49 +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 (只读,无操作按钮) |