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

17 KiB
Raw Blame History

切片 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.tsdecodeJwtPayload 提取权限码列表,存入 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 之后):
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 [];
  }
}
  1. 修改 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: [] };
}
  1. 修改 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;
}
  1. 修改 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: 功能验证
  1. 用管理员账号登录 → 所有按钮可见
  2. 创建一个无权限的测试角色(仅 health.patient.list)→ 分配给测试用户
  3. 用测试用户登录 → 仅患者列表可见,新建/编辑/删除按钮隐藏
  4. 确认表格行点击导航(如患者详情页)仍然正常
  • 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 (只读,无操作按钮)