对应 5 份设计规格,共 75 个 Task: 1. 性能优化 (12 Task) — 批量INSERT/N+1内联name/合并COUNT/按需重绘/chunk拆分 2. 安全纵深防御 (8 Task) — RLS/行级数据范围/Redis session_key/审计哈希链 3. 事件驱动架构 (10 Task) — 11个缺失事件补发/LISTEN+NOTIFY/schema版本化 4. 前端工程化 (10 Task) — hook统一/组件拆分/Bundle优化 5. 可观测性运维 (10 Task) — 深度健康检查/Prometheus/OTel/生产Docker/告警
291 lines
11 KiB
Markdown
291 lines
11 KiB
Markdown
# 前端工程化改进实施计划
|
||
|
||
> 设计规格: `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: <T>(fn: () => Promise<T>, successMsg?: string) => Promise<T | null>;
|
||
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<T, F = string>(
|
||
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<Record<string, string>>` 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<Record<string, string>>`
|
||
- `batchResolveDoctorNames(ids: string[]): Promise<Record<string, string>>`
|
||
2. 内部实现:去重 → 过滤已缓存 → 并发加载(限制 5 并发)→ 写入缓存并返回
|
||
3. 在 AppointmentList 中移除 nameCache state,改用 store 方法
|
||
4. 在 PointsOrderList 中同样迁移
|
||
|
||
**验收标准**:
|
||
- 两个页面无 `useState<Record<string, string>>` 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. **渐进迁移** — 重复模式统一采用渐进策略,不一次性全量迁移
|