diff --git a/docs/superpowers/plans/2026-04-25-slice1-button-permissions.md b/docs/superpowers/plans/2026-04-25-slice1-button-permissions.md new file mode 100644 index 0000000..fcbb43f --- /dev/null +++ b/docs/superpowers/plans/2026-04-25-slice1-button-permissions.md @@ -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`(后端登录时写入),前端复用 `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; + logout: () => Promise; + loadFromStorage: () => void; +} +``` + +4. 修改 store 创建,初始化 `permissions`,在 login/logout 中同步更新: + +```typescript +export const useAuthStore = create((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 行 `新建患者` 按钮 → `` +- 第 242-267 行 操作列的编辑/删除按钮 → `` + +- [ ] **Step 1: 添加 import** + +在 PatientList.tsx 顶部 import 区域添加: + +```typescript +import { AuthButton } from '../../components/AuthButton'; +``` + +- [ ] **Step 2: 包裹新建患者按钮** + +将第 304 行: +```tsx + +``` + +改为: +```tsx + + + +``` + +- [ ] **Step 3: 包裹操作列按钮** + +将 columns 操作列的 render(第 241-270 行): +```tsx +render: (_: unknown, record: PatientListItem) => ( + +