18 个 Task,2 个 Chunk: - Chunk 1: 权限基础设施(JWT 解码 + store + hook + 组件) - Chunk 2: 健康模块 15 页面按钮改造 + 集成验证
17 KiB
切片 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 中:
- 新增辅助函数(文件顶部,import 之后):
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 [];
}
}
- 修改
restoreInitialState返回值,增加permissions:
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: [] };
}
- 修改
AuthState接口,增加permissions:
interface AuthState {
user: UserInfo | null;
isAuthenticated: boolean;
loading: boolean;
permissions: string[];
login: (username: string, password: string) => Promise<void>;
logout: () => Promise<void>;
loadFromStorage: () => void;
}
- 修改 store 创建,初始化
permissions,在 login/logout 中同步更新:
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: 提交
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
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: 提交
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:
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:
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: 提交
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 区域添加:
import { AuthButton } from '../../components/AuthButton';
- Step 2: 包裹新建患者按钮
将第 304 行:
<Button type="primary" icon={<PlusOutlined />} onClick={openCreateModal}>
新建患者
</Button>
改为:
<AuthButton code="health.patient.manage">
<Button type="primary" icon={<PlusOutlined />} onClick={openCreateModal}>
新建患者
</Button>
</AuthButton>
- Step 3: 包裹操作列按钮
将 columns 操作列的 render(第 241-270 行):
render: (_: unknown, record: PatientListItem) => (
<Space size={4}>
<Button ... />
<Popconfirm ...><Button ... /></Popconfirm>
</Space>
),
改为:
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: 提交
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: 提交
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: 验证 + 提交
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: 验证 + 提交
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: 验证 + 提交
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: 验证 + 提交
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: 验证 + 提交
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: 验证 + 提交
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: 验证 + 提交
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: 验证 + 提交
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: 验证 + 提交
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: 验证 + 提交
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: 验证 + 提交
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: 验证 + 提交
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: 功能验证
- 用管理员账号登录 → 所有按钮可见
- 创建一个无权限的测试角色(仅
health.patient.list)→ 分配给测试用户 - 用测试用户登录 → 仅患者列表可见,新建/编辑/删除按钮隐藏
- 确认表格行点击导航(如患者详情页)仍然正常
- Step 4: 生产构建验证
Run: cd apps/web && pnpm build
Expected: 构建成功
- Step 5: 推送所有提交
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 (只读,无操作按钮) |