# 前端工程化改进实施计划 > 设计规格: `docs/superpowers/specs/2026-04-26-frontend-engineering-design.md` > 日期: 2026-04-26 | 状态: draft | 总周期: 7 天 --- ## Phase 1: 重复模式统一(Day 1-2) ### Task 1: 增强 useApiRequest hook,统一错误处理 **目标**: 补齐 loading 状态,消除组件内联 `catch (err) { message.error(...) }` 模式。 **涉及文件**: - 修改: `apps/web/src/hooks/useApiRequest.ts` **详细步骤**: 1. 在 `useApiRequest` 返回值中新增 `loading: boolean` 状态: ```typescript interface UseApiRequestReturn { execute: (fn: () => Promise, successMsg?: string) => Promise; loading: boolean; } ``` 2. `execute` 内部在调用前 `setLoading(true)`,finally 中 `setLoading(false)` 3. 保持现有调用点无需修改 — 返回值是对象解构,新增字段不影响旧代码 4. 选取 3 个健康模块页面(PatientList、AppointmentList、FollowUpTaskList)迁移为使用 `execute` + `loading` **验收标准**: - `pnpm build` 通过 - 3 个迁移页面的 catch 块不再有内联 `message.error`,统一走 `handleApiError` - loading 状态正确绑定到页面按钮/Spin 组件 --- ### Task 2: 增强 usePaginatedData hook,健康模块页面迁移 **目标**: 支持泛型筛选参数,迁移 6 个健康列表页使用统一 hook。 **涉及文件**: - 修改: `apps/web/src/hooks/usePaginatedData.ts` - 修改: `apps/web/src/pages/health/PatientList.tsx` - 修改: `apps/web/src/pages/health/OfflineEventList.tsx` - 修改: `apps/web/src/pages/health/PointsProductList.tsx` **详细步骤**: 1. 增强 hook 签名为泛型筛选: ```typescript function usePaginatedData( fetchFn: (page: number, pageSize: number, filters: F) => Promise<{ data: T[]; total: number }>, options?: { pageSize?: number; defaultFilters: F; autoFetch?: boolean } ): { data, total, page, loading, filters, setFilters, refresh } ``` 2. 函数重载保持旧 `(fetchFn, pageSize?)` 签名兼容 3. 新增 `filters` / `setFilters` 状态,`fetchFn` 调用时传入当前 filters 4. 迁移 PatientList(按 status/name/gender 筛选)和 OfflineEventList(按 status/dateRange 筛选) **验收标准**: - 旧调用点(不传 filters)行为不变 - PatientList 和 OfflineEventList 筛选功能正常,代码行数各减少 15-25 行 - `pnpm build` 通过 --- ### Task 3: 移除 nameCache,统一用 useHealthStore **目标**: 消除 AppointmentList 和 PointsOrderList 自建的 `useState>` nameCache。 **涉及文件**: - 修改: `apps/web/src/stores/health.ts` - 修改: `apps/web/src/pages/health/AppointmentList.tsx` - 修改: `apps/web/src/pages/health/PointsOrderList.tsx` **详细步骤**: 1. 在 `useHealthStore` 新增批量解析方法: - `batchResolvePatientNames(ids: string[]): Promise>` - `batchResolveDoctorNames(ids: string[]): Promise>` 2. 内部实现:去重 → 过滤已缓存 → 并发加载(限制 5 并发)→ 写入缓存并返回 3. 在 AppointmentList 中移除 nameCache state,改用 store 方法 4. 在 PointsOrderList 中同样迁移 **验收标准**: - 两个页面无 `useState>` nameCache 代码 - 患者姓名/医生姓名在列表中正确显示 - `pnpm build` 通过 --- ## Phase 2: 大组件拆分(Day 3-5) ### Task 4: PluginCRUDPage 拆分为 CRUDTable/CRUDForm/DetailDrawer/ImportExport **目标**: 将 872 行的 PluginCRUDPage.tsx 拆为容器 + 展示组件。 **涉及文件**: - 新增: `apps/web/src/pages/plugins/components/CRUDTable.tsx` (~150 行) - 新增: `apps/web/src/pages/plugins/components/CRUDForm.tsx` (~180 行) - 新增: `apps/web/src/pages/plugins/components/DetailDrawer.tsx` (~80 行) - 新增: `apps/web/src/pages/plugins/components/ImportExport.tsx` (~100 行) - 新增: `apps/web/src/pages/plugins/hooks/usePluginData.ts` (~120 行) - 修改: `apps/web/src/pages/plugins/PluginCRUDPage.tsx` (缩减至 ~80 行) **详细步骤**: 1. 创建 `hooks/usePluginData.ts`:提取 CRUD 操作、导入导出逻辑、Drawer 可见性状态 2. 创建 `CRUDTable.tsx`:表格列定义 + 行操作按钮,props 接收 data/onDelete/onEdit/onDetail 3. 创建 `CRUDForm.tsx`:新增/编辑表单 + Drawer,包含校验规则 4. 创建 `DetailDrawer.tsx`:详情展示 + 操作历史 Timeline 5. 创建 `ImportExport.tsx`:导入面板 + 导出按钮 6. 改写 `PluginCRUDPage.tsx` 为容器组件:调用 usePluginData hook,组装子组件 **验收标准**: - `pnpm build` 通过 - 插件 CRUD 所有功能正常(新增、编辑、删除、详情、导入、导出) - PluginCRUDPage.tsx <= 100 行,无子组件超过 200 行 --- ### Task 5: PluginGraphPage 抽取 useGraphCanvas hook **目标**: 将 759 行的 PluginGraphPage.tsx 拆为 hook + 展示组件。 **涉及文件**: - 新增: `apps/web/src/pages/plugins/hooks/useGraphLayout.ts` (~100 行) - 新增: `apps/web/src/pages/plugins/hooks/useGraphData.ts` (~80 行) - 新增: `apps/web/src/pages/plugins/components/GraphCanvas.tsx` (~200 行) - 新增: `apps/web/src/pages/plugins/components/GraphToolbar.tsx` (~60 行) - 修改: `apps/web/src/pages/plugins/PluginGraphPage.tsx` (缩减至 ~60 行) **详细步骤**: 1. `useGraphData.ts`:数据加载、边/节点格式转换、字段映射 2. `useGraphLayout.ts`:Dagre/elkjs 布局算法、节点位置计算、自动布局触发 3. `GraphCanvas.tsx`:ReactFlow 渲染、自定义节点样式、拖拽交互 4. `GraphToolbar.tsx`:缩放控制、自动布局、布局方向切换 5. 容器组件组装以上模块 **验收标准**: - 插件关系图页面正常渲染和交互 - 拖拽节点、自动布局、缩放功能正常 - `pnpm build` 通过 --- ### Task 6: Organizations.tsx 抽象 TreeEntityManager **目标**: 将 622 行的 Organizations.tsx 按三层模式拆分。 **涉及文件**: - 新增: `apps/web/src/pages/system/hooks/useOrgTree.ts` (~80 行) - 新增: `apps/web/src/pages/system/components/OrgTree.tsx` (~120 行) - 新增: `apps/web/src/pages/system/components/OrgDetail.tsx` (~150 行) - 新增: `apps/web/src/pages/system/components/DeptMemberList.tsx` (~100 行) - 修改: `apps/web/src/pages/system/Organizations.tsx` (缩减至 ~60 行) **详细步骤**: 1. `useOrgTree.ts`:树数据加载、CRUD 操作、选中节点状态 2. `OrgTree.tsx`:左侧树形选择(DirectoryTree + 搜索 + 右键菜单) 3. `OrgDetail.tsx`:右侧组织详情/编辑表单 4. `DeptMemberList.tsx`:部门成员列表 + 人员分配 Modal 5. 容器组件三栏布局组装 **验收标准**: - 组织管理 CRUD 功能正常(新增/编辑/删除组织、部门、人员分配) - 树形选择、搜索过滤正常 - `pnpm build` 通过 --- ### Task 7: StatisticsDashboard 拆分为独立卡片组件 **目标**: 将 580 行的 StatisticsDashboard.tsx 拆为 hook + 独立图表卡片。 **涉及文件**: - 新增: `apps/web/src/pages/health/hooks/useStatsData.ts` (~100 行) - 新增: `apps/web/src/pages/health/components/PatientTrendChart.tsx` (~80 行) - 新增: `apps/web/src/pages/health/components/AppointmentStats.tsx` (~80 行) - 新增: `apps/web/src/pages/health/components/OverviewCards.tsx` (~60 行) - 新增: `apps/web/src/pages/health/components/TimeRangeSelector.tsx` (~40 行) - 修改: `apps/web/src/pages/health/StatisticsDashboard.tsx` (缩减至 ~50 行) **详细步骤**: 1. `useStatsData.ts`:五个统计 API 并行加载、loading/error 状态、时间范围变更触发刷新 2. `PatientTrendChart.tsx`:患者趋势折线图(@ant-design/charts Line) 3. `AppointmentStats.tsx`:预约统计饼图/柱状图 4. `OverviewCards.tsx`:概览数字卡片组(Statistic + Card) 5. `TimeRangeSelector.tsx`:日期范围选择 + 快捷选项(近7天/近30天/近90天) 6. 容器组件组装,布局使用 Row + Col **验收标准**: - 统计仪表板页面渲染正常,图表数据正确 - 时间范围切换触发数据刷新 - `pnpm build` 通过 --- ## Phase 3: Bundle 优化(Day 6-7) ### Task 8: vite.config.ts manualChunks 拆分重型依赖 **目标**: 将 @ant-design/charts、@xyflow/react、@wangeditor/editor 拆为独立 chunk,降低主 chunk 体积。 **涉及文件**: - 修改: `apps/web/vite.config.ts` **详细步骤**: 1. 在 `manualChunks` 配置中新增三条规则: ```typescript if (id.includes('@ant-design/charts') || id.includes('@antv/')) return 'vendor-charts'; if (id.includes('@xyflow/react') || id.includes('@reactflow/')) return 'vendor-flow'; if (id.includes('@wangeditor/')) return 'vendor-editor'; ``` 2. 对应页面添加路由级 `React.lazy()`: - `StatisticsDashboard` → `lazy(() => import('./health/StatisticsDashboard'))` - `PluginGraphPage` → `lazy(() => import('./plugins/PluginGraphPage'))` - `ArticleEditor` → `lazy(() => import('./health/ArticleEditor'))` 3. 将 `chunkSizeWarningLimit` 从 600 降至 500 4. 运行 `pnpm build` 对比拆分前后各 chunk 大小 **验收标准**: - 主 chunk 体积 < 400KB(gzip 前约 600KB 以内) - `vendor-charts`、`vendor-flow`、`vendor-editor` 独立生成 - `pnpm build` 无警告 - 统计仪表板、插件关系图、文章编辑器页面功能正常(懒加载无闪烁) --- ### Task 9: columns 配置 useMemo 化 **目标**: 消除 PluginCRUDPage 和健康模块列表页的 columns 重复创建,减少不必要的 re-render。 **涉及文件**: - 修改: `apps/web/src/pages/plugins/components/CRUDTable.tsx`(Phase 2 Task 4 产物) - 修改: `apps/web/src/pages/health/PatientList.tsx` - 修改: `apps/web/src/pages/health/AppointmentList.tsx` - 修改: `apps/web/src/pages/health/FollowUpTaskList.tsx` **详细步骤**: 1. 在每个列表页中,将 `columns` 数组定义包裹在 `useMemo` 中 2. 依赖项包含 columns 中引用的回调函数(如 onDelete、onEdit) 3. 确保回调函数通过 `useCallback` 缓存,避免 useMemo 失效 4. 使用 React DevTools Profiler 验证翻页/筛选时减少不必要渲染 **验收标准**: - 列表翻页时 Table 组件不因 columns 引用变化触发全量渲染 - 所有列表页功能正常(排序、筛选、操作按钮) - `pnpm build` 通过 --- ### Task 10: API 层新代码统一为对象风格 **目标**: 确认新增 API 文件采用对象风格(`xxxApi.list()` 而非 `listXxx()`),修改已有文件时顺手迁移。 **涉及文件**: - 修改: `apps/web/src/api/health/` 下近期新增的 API 文件(如 `alerts.ts`、`deviceReadings.ts`) **详细步骤**: 1. 审计 `apps/web/src/api/` 下所有文件,标记函数风格的文件清单 2. 近期新增的文件(alerts、deviceReadings 等)统一改为对象风格: ```typescript export const alertApi = { list: (params) => client.get('/alerts', { params }), acknowledge: (id) => client.post(`/alerts/${id}/acknowledge`), }; ``` 3. 更新引用处的 import(页面组件中的调用方式) 4. 旧文件不强制迁移,仅记录待迁移清单 **验收标准**: - `alerts.ts` 和 `deviceReadings.ts` 为对象风格导出 - 对应页面功能正常 - `pnpm build` 通过 --- ## 执行原则 1. **每 Task 完成后立即提交** — 不积压,保持可追溯 2. **先基础设施后拆分** — Phase 1 的 hook 增强完成后再做 Phase 2 组件拆分 3. **每步验证** — 每个 Task 完成后 `pnpm build` 验证,拆分任务额外验证页面功能 4. **渐进迁移** — 重复模式统一采用渐进策略,不一次性全量迁移