Files
hms/docs/superpowers/plans/2026-04-28-ui-ux-overhaul-plan.md
iven 16a776c213
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: UI/UX 重构实施计划 — 6 Phase 37 Task 分步详述
Phase 1 基础组件提取 → Phase 2 仪表盘角色自适应 → Phase 3 列表页统一
→ Phase 4 表单升级 Drawer → Phase 5 小程序重构 → Phase 6 验收
2026-04-28 01:42:50 +08:00

1063 lines
35 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# HMS UI/UX 全面重构 — 实施计划
> **日期**: 2026-04-28 | **依赖规格**: `docs/superpowers/specs/2026-04-28-ui-ux-overhaul-design.md`
> **总工期**: 12-17 天 | **6 Phase · 28 Task**
## 前置条件
- [x] 设计规格已通过审查
- [x] 角色代码映射已确认
- [x] 技术栈antd 6.x + dayjs + ZustandWeb、Taro 4.2 + React 18小程序
---
## Phase 1: 基础组件提取1-2天
> 目标:建立共享基础设施,后续 Phase 全部依赖。
| # | Task | 产出文件 | 依赖 |
|---|------|---------|------|
| 1.1 | dayjs 集中初始化 | `apps/web/src/utils/dayjs.ts` | — |
| 1.2 | 日期/年龄格式化工具 | `apps/web/src/utils/format.ts` | 1.1 |
| 1.3 | EntityName 兜底组件 | `apps/web/src/components/EntityName.tsx` | — |
| 1.4 | FilterBar 筛选栏组件 | `apps/web/src/components/FilterBar.tsx` | — |
| 1.5 | PageContainer 页面容器 | `apps/web/src/components/PageContainer.tsx` | 1.2, 1.4 |
| 1.6 | DrawerForm 抽屉表单 | `apps/web/src/components/DrawerForm.tsx` | — |
**验证**: `pnpm build` 通过 + 现有页面无回归
---
## Phase 2: 仪表盘重构2-3天
> 目标:替换现有 7 区块仪表盘为 4 角色自适应视图。
| # | Task | 产出文件 | 依赖 |
|---|------|---------|------|
| 2.1 | 后端:个人工作量 API | Rust handler + service | — |
| 2.2 | 前端:角色判定 hook | `apps/web/src/hooks/useDashboardRole.ts` | — |
| 2.3 | 医生视图组件 | `StatisticsDashboard/DoctorDashboard.tsx` | 2.1, 2.2 |
| 2.4 | 护士视图组件 | `StatisticsDashboard/NurseDashboard.tsx` | 2.1, 2.2 |
| 2.5 | 管理员视图组件 | `StatisticsDashboard/AdminDashboard.tsx` | 2.2 |
| 2.6 | 运营视图组件 | `StatisticsDashboard/OperatorDashboard.tsx` | 2.2 |
| 2.7 | 路由组件 + 删除旧区块 | `StatisticsDashboard/index.tsx` | 2.3-2.6 |
**验证**: 逐角色登录,确认各视图正确渲染
---
## Phase 3: 列表页统一3-4天
> 目标:所有列表页迁移到 PageContainer统一筛选/列/格式。
| # | Task | 产出文件 | 依赖 |
|---|------|---------|------|
| 3.1 | 患者管理页迁移 | `PatientList.tsx` | Phase 1 |
| 3.2 | 医生管理页迁移 | `DoctorList.tsx` | Phase 1 |
| 3.3 | 预约管理页迁移 | `AppointmentList.tsx` | Phase 1 |
| 3.4 | 随访任务页迁移 | `FollowUpTaskList.tsx` | Phase 1 |
| 3.5 | 咨询管理页迁移 | `ConsultationList.tsx` | Phase 1 |
| 3.6 | 积分(规则/商品/订单)迁移 | 3 个页面 | Phase 1 |
| 3.7 | 告警列表迁移 | `AlertList.tsx` | Phase 1 |
| 3.8 | 文章管理页迁移 | `ArticleList.tsx` | Phase 1 |
**验证**: 逐页面对比截图,确认筛选/列/格式改进生效
---
## Phase 4: 表单升级2-3天
> 目标:中等复杂度表单迁移到 DrawerForm分组+双列布局。
| # | Task | 产出文件 | 依赖 |
|---|------|---------|------|
| 4.1 | 患者表单Modal → Drawer + 分组 | `PatientList.tsx` 内表单 | 1.6, 3.1 |
| 4.2 | 预约表单Drawer + 排班校验 | `AppointmentList.tsx` 内表单 | 1.6, 3.3 |
| 4.3 | 随访填写Drawer + 完整回填 | `FollowUpTaskList.tsx` 内表单 | 1.6, 3.4 |
| 4.4 | 积分商品Drawer + 图片上传 | 积分商品页内表单 | 1.6, 3.6 |
**验证**: 每个表单创建/编辑完整流程测试
---
## Phase 5: 小程序重构3-4天
> 目标8 个页面按设计规格优化 UI/UX。
| # | Task | 产出文件 | 依赖 |
|---|------|---------|------|
| 5.1 | 患者首页重设计 | `pages/index/index.tsx` | — |
| 5.2 | 健康数据 Hub 优化 | `pages/health/index.tsx` | — |
| 5.3 | 日常监测表单分组 | `pages/daily-monitoring/index.tsx` | — |
| 5.4 | 预约流程优化 | `pages/appointment/create/index.tsx` | — |
| 5.5 | 咨询聊天优化 | `pages/consultation/detail/index.tsx` | — |
| 5.6 | 积分商城优化 | `pages/points/` 相关页面 | — |
| 5.7 | 医护工作台重设计 | `pages/doctor/index.tsx` | — |
| 5.8 | 趋势图表优化 | `components/TrendChart/` | — |
**验证**: 小程序编译 + 真机预览
---
## Phase 6: 验收1天
| # | Task | 范围 |
|---|------|------|
| 6.1 | 暗色模式全量测试 | Web 所有页面 + 小程序 |
| 6.2 | 响应式布局测试 | 1280/1440/1920 宽度 |
| 6.3 | 各角色权限测试 | 4 角色逐个登录验证 |
| 6.4 | 生产构建验证 | `pnpm build` + `cargo build --release` |
---
(以下逐章展开详细实施步骤)
---
## Phase 1 详细步骤:基础组件提取
### Task 1.1 — dayjs 集中初始化
**文件**: `apps/web/src/utils/dayjs.ts`(新建)
**步骤**:
1. 创建 `utils/dayjs.ts`,导入 dayjs + relativeTime 插件 + zh-cn locale
2. 执行 `dayjs.extend(relativeTime)``dayjs.locale('zh-cn')`
3. 导出已初始化的 `dayjs` 实例
**代码骨架**:
```typescript
import dayjs from 'dayjs';
import relativeTime from 'dayjs/plugin/relativeTime';
import 'dayjs/locale/zh-cn';
dayjs.extend(relativeTime);
dayjs.locale('zh-cn');
export { dayjs };
export default dayjs;
```
**清理**: 全局搜索 `import dayjs from 'dayjs'``import relativeTime from 'dayjs/plugin/relativeTime'` 的独立初始化(已知 `AlertList.tsx` 第 13-14 行),替换为 `import { dayjs } from '@/utils/dayjs'`
**验证**: `pnpm build` 通过
---
### Task 1.2 — 日期/年龄格式化工具
**文件**: `apps/web/src/utils/format.ts`(新建)
**步骤**:
1.`./dayjs` 导入已初始化的 dayjs
2. 导出 4 个函数:`formatDate``formatDateTime``formatRelative``calcAge`
**代码骨架**:
```typescript
import { dayjs } from './dayjs';
export const formatDate = (v: string | null | undefined): string =>
v ? dayjs(v).format('YYYY-MM-DD') : '--';
export const formatDateTime = (v: string | null | undefined): string =>
v ? dayjs(v).format('YYYY-MM-DD HH:mm') : '--';
export const formatRelative = (v: string | null | undefined): string =>
v ? dayjs(v).fromNow() : '--';
export const calcAge = (birthDate: string | null | undefined): string => {
if (!birthDate) return '--';
const age = dayjs().diff(dayjs(birthDate), 'year');
return `${age}`;
};
```
**验证**: `pnpm build` 通过
---
### Task 1.3 — EntityName 兜底组件
**文件**: `apps/web/src/components/EntityName.tsx`(新建)
**步骤**:
1. 创建组件,接收 `name``id``fallbackLabel` props
2. name 存在时直接显示
3. name 缺失时显示灰色 fallback 文字 + Tooltip 显示 ID
**代码骨架**:
```typescript
import { Tooltip, Typography } from 'antd';
interface EntityNameProps {
name?: string | null;
id?: string;
fallbackLabel?: string; // 默认 "未知"
}
export function EntityName({ name, id, fallbackLabel = '未知' }: EntityNameProps) {
if (name) return <span>{name}</span>;
return (
<Tooltip title={id ? `ID: ${id}` : undefined}>
<Typography.Text type="secondary">{fallbackLabel}</Typography.Text>
</Tooltip>
);
}
```
**验证**: `pnpm build` 通过
---
### Task 1.4 — FilterBar 筛选栏组件
**文件**: `apps/web/src/components/FilterBar.tsx`(新建)
**步骤**:
1. 创建组合式筛选栏容器
2. 左侧渲染 children筛选控件右侧渲染"重置"按钮 + extra 区域
3. 使用 Ant Design Space + Flex 布局,支持暗色模式
**代码骨架**:
```typescript
import { Button, Flex, Space } from 'antd';
import { ReloadOutlined } from '@ant-design/icons';
import { useThemeMode } from '../hooks/useThemeMode';
interface FilterBarProps {
children: React.ReactNode;
onReset?: () => void;
extra?: React.ReactNode;
}
export function FilterBar({ children, onReset, extra }: FilterBarProps) {
const isDark = useThemeMode();
return (
<Flex justify="space-between" align="center"
style={{ padding: '12px 16px', background: isDark ? '#1f1f1f' : '#fafafa', borderRadius: 8, marginBottom: 16 }}>
<Space wrap>{children}</Space>
<Space>
{onReset && <Button icon={<ReloadOutlined />} onClick={onReset}></Button>}
{extra}
</Space>
</Flex>
);
}
```
**验证**: `pnpm build` 通过
---
### Task 1.5 — PageContainer 页面容器
**文件**: `apps/web/src/components/PageContainer.tsx`(新建)
**步骤**:
1. 创建统一页面容器,集成标题栏 + FilterBar + 批量操作栏 + 表格插槽
2. 暗色模式自动适配
3. 批量操作栏在有选中项时显示,替换常规操作栏
**代码骨架**:
```typescript
import { Card, Flex, Space, Typography, Button } from 'antd';
import { useThemeMode } from '../hooks/useThemeMode';
import { FilterBar } from './FilterBar';
interface PageContainerProps {
title: string;
subtitle?: string;
filters?: React.ReactNode;
onResetFilters?: () => void;
filterExtra?: React.ReactNode;
actions?: React.ReactNode;
batchActions?: React.ReactNode;
selectedCount?: number;
onClearSelection?: () => void;
children: React.ReactNode; // 表格或其他内容
loading?: boolean;
}
export function PageContainer({
title, subtitle, filters, onResetFilters, filterExtra,
actions, batchActions, selectedCount, onClearSelection, children, loading,
}: PageContainerProps) {
const isDark = useThemeMode();
return (
<div style={{ padding: 24 }}>
{/* 标题栏 */}
<Flex justify="space-between" align="center" style={{ marginBottom: 16 }}>
<div>
<Typography.Title level={4} style={{ margin: 0 }}>{title}</Typography.Title>
{subtitle && <Typography.Text type="secondary">{subtitle}</Typography.Text>}
</div>
<Space>
{selectedCount ? batchActions : actions}
{selectedCount && onClearSelection && (
<Button size="small" onClick={onClearSelection}> ({selectedCount})</Button>
)}
</Space>
</Flex>
{/* 筛选栏 */}
{filters && <FilterBar onReset={onResetFilters} extra={filterExtra}>{filters}</FilterBar>}
{/* 内容区 */}
<Card styles={{ body: { padding: 0 } }}
style={{ background: isDark ? '#141414' : '#fff' }} loading={loading}>
{children}
</Card>
</div>
);
}
```
**验证**: `pnpm build` 通过。可选:选取一个列表页临时引入 PageContainer确认布局正确。
---
### Task 1.6 — DrawerForm 抽屉表单
**文件**: `apps/web/src/components/DrawerForm.tsx`(新建)
**步骤**:
1. 创建 Ant Design Drawer + Form 组合组件
2. 支持分组模式sections和非分组模式children
3. 双列网格布局columns prop
4. 编辑时完整回填initialValues + form.setFieldsValue
**代码骨架**:
```typescript
import { Drawer, Form, Typography, Divider } from 'antd';
import { useThemeMode } from '../hooks/useThemeMode';
interface FormSection {
title: string;
fields: React.ReactNode;
defaultCollapsed?: boolean;
}
interface DrawerFormProps {
title: string;
open: boolean;
onClose: () => void;
onSubmit: (values: Record<string, unknown>) => Promise<void>;
initialValues?: Record<string, unknown>;
loading?: boolean;
width?: number | string;
sections?: FormSection[];
children?: React.ReactNode;
columns?: 1 | 2;
}
export function DrawerForm({
title, open, onClose, onSubmit, initialValues,
loading, width = 640, sections, children, columns = 2,
}: DrawerFormProps) {
const [form] = Form.useForm();
const isDark = useThemeMode();
// initialValues 变化时回填
React.useEffect(() => { form.resetFields(); if (initialValues) form.setFieldsValue(initialValues); }, [initialValues, form]);
const handleSubmit = async () => {
const values = await form.validateFields();
await onSubmit(values);
};
const gridStyle = columns === 2 ? { display: 'grid', gridTemplateColumns: '1fr 1fr', gap: '0 16px' } : {};
return (
<Drawer title={title} open={open} onClose={onClose} width={width}
extra={<Space><Button onClick={onClose}></Button><Button type="primary" onClick={handleSubmit} loading={loading}></Button></Space>}>
<Form form={form} layout="vertical" initialValues={initialValues}>
{sections ? sections.map((s, i) => (
<div key={i}>
{i > 0 && <Divider />}
<Typography.Text strong style={{ fontSize: 14, marginBottom: 12, display: 'block' }}>{s.title}</Typography.Text>
<div style={gridStyle}>{s.fields}</div>
</div>
)) : <div style={gridStyle}>{children}</div>}
</Form>
</Drawer>
);
}
```
**验证**: `pnpm build` 通过
---
### Phase 1 整体验证
1. `cd apps/web && pnpm build` — 生产构建通过
2. 启动后端 + 前端,确认现有页面无回归(不引入新组件,仅新增文件)
3. 提交:`feat(web): 提取 PageContainer/EntityName/DrawerForm/FilterBar 共享组件 + format 工具`
---
## Phase 2 详细步骤:仪表盘重构
### Task 2.1 — 后端:个人工作量 API
**文件**:
- `crates/erp-health/src/handlers/dashboard.rs`(新建或追加)
- `crates/erp-health/src/services/dashboard.rs`(新建或追加)
- `crates/erp-health/src/routes.rs`(追加路由)
**步骤**:
1. 定义 `PersonalStatsResponse` 结构体(字段见规格 1.7.1
2. 实现 `DashboardService::personal_stats(user_id, tenant_id)` 查询方法
3. 数据来源:从 `patients`doctor_id 筛选)、`follow_up_tasks``consultations``vital_signs` 表聚合计算
4. 注册路由 `GET /api/v1/health/dashboard/personal-stats`
5. 添加 utoipa 注解
**SQL 聚合逻辑**:
```sql
-- 我的患者数
SELECT COUNT(*) FROM patients WHERE doctor_id = $1 AND tenant_id = $2 AND deleted_at IS NULL;
-- 本月新增
SELECT COUNT(*) FROM patients WHERE doctor_id = $1 AND tenant_id = $2 AND created_at >= $month_start;
-- 随访完成率
SELECT COUNT(*) FILTER (WHERE status = 'completed') * 100.0 / COUNT(*) FROM follow_up_tasks WHERE ...;
-- 今日待随访
SELECT COUNT(*) FROM follow_up_tasks WHERE assignee_id = $1 AND planned_date = CURRENT_DATE;
-- 待审核化验
SELECT COUNT(*) FROM lab_reports WHERE reviewed_by IS NULL AND tenant_id = $2;
```
**验证**: `cargo test` + Swagger UI 调用返回正确数据
---
### Task 2.2 — 前端:角色判定 hook
**文件**: `apps/web/src/hooks/useDashboardRole.ts`(新建)
**步骤**:
1. 从 Zustand auth store 读取 `user.roles`
2. 按优先级 `doctor > nurse > admin > operator` 匹配
3. 返回匹配的角色类型 `'doctor' | 'nurse' | 'admin' | 'operator'`
**代码骨架**:
```typescript
import { useAuthStore } from '../stores/auth';
type DashboardRole = 'doctor' | 'nurse' | 'admin' | 'operator';
const ROLE_PRIORITY: DashboardRole[] = ['doctor', 'nurse', 'admin', 'operator'];
export function useDashboardRole(): DashboardRole {
const user = useAuthStore(s => s.user);
if (!user?.roles?.length) return 'admin'; // 无角色默认管理员视图
const codes = user.roles.map(r => r.code);
for (const role of ROLE_PRIORITY) {
if (codes.some(c => c === role || c.startsWith(role))) return role;
}
return 'admin';
}
```
**验证**: `pnpm build` 通过
---
### Task 2.3 — 医生视图组件
**文件**: `apps/web/src/pages/health/StatisticsDashboard/DoctorDashboard.tsx`(新建)
**步骤**:
1. 调用 `personal-stats` API + `useStatsData`(全局统计)
2. 布局:顶部问候 + 紧急提醒 → 中间双列(日程 + 化验/咨询)→ 底部统计卡
3. 使用 Ant Design Card + Row/Col 布局
4. 日程区显示今日预约时间线,当前时间高亮
5. 化验审核列表显示待审核项(异常项红色标记)
6. 底部 4 个统计卡使用 `<Statistic>` 组件 + 趋势箭头
**数据源**:
- `personal-stats` API → 统计卡数据
- 现有预约 API → 今日日程
- 现有化验 API → 待审核列表
- 现有咨询 API → 未读消息
**验证**: 以医生角色登录,确认日程/审核/消息/统计正确渲染
---
### Task 2.4 — 护士视图组件
**文件**: `apps/web/src/pages/health/StatisticsDashboard/NurseDashboard.tsx`(新建)
**步骤**:
1. 布局:问候 + 异常提醒 → 双列(随访队列 + 上报率)→ 统计卡
2. 异常提醒区:体征异常患者列表,每项可"通知医生"
3. 随访队列:按逾期/今日分组,显示患者名 + 随访类型
4. 上报率:环形进度条 + 已上报/未上报人数 + 催报按钮
5. 底部 3 个统计卡(今日预约/随访完成率/逾期随访)
**数据源**:
- `personal-stats` API → 统计数据
- 体征 API → 异常列表(按阈值筛选)
- 随访 API → 今日/逾期队列
**验证**: 以护士角色登录,确认队列/上报率/异常正确显示
---
### Task 2.5 — 管理员视图组件
**文件**: `apps/web/src/pages/health/StatisticsDashboard/AdminDashboard.tsx`(新建)
**步骤**:
1. 布局:顶部 5 KPI 卡 → 中间双列(趋势图 + 工作量)→ 底部 Tab
2. KPI 卡:患者总数、本月预约、随访完成率、体征上报率、医护人数
3. 趋势图:复用现有 ECharts 或轻量级图表(双线图:新增患者 + 预约数)
4. 工作量:横向条形图,显示各医护的患者数/随访完成率
5. 底部 Tab透析管理 / 化验报告 / 预约分析 / 体征数据(复用现有 HealthDataCenter 组件)
**数据源**:
- `useStatsData` hook → 现有全局统计
- `HealthDataCenter.tsx` → 底部 Tab 内容
**验证**: 以管理员角色登录,确认 KPI/图表/Tab 正确渲染
---
### Task 2.6 — 运营视图组件
**文件**: `apps/web/src/pages/health/StatisticsDashboard/OperatorDashboard.tsx`(新建)
**步骤**:
1. 布局:顶部 4 KPI → 中间双列(积分排行 + 热门文章)→ 底部活动
2. KPI 卡:积分发放/消费、文章发布、活动报名
3. 积分排行Top 5 列表(从现有 useStatsData.pointsStats 获取)
4. 热门文章Top 3 列表(从文章 API 获取,按浏览量排序)
5. 底部活动:进行中的线下活动卡片
**数据源**:
- `useStatsData` hook → 积分统计
- 文章 API → 热门文章
- 活动 API → 进行中活动
**验证**: 以运营角色登录,确认积分/文章/活动正确渲染
---
### Task 2.7 — 路由组件 + 删除旧区块
**文件**: `apps/web/src/pages/health/StatisticsDashboard.tsx`(重写)
**步骤**:
1. 导入 `useDashboardRole` hook
2. 根据 role 条件渲染对应的 Dashboard 组件
3. 删除旧内容:快捷入口区块、积分排行 Top10、最近活动区块
4. 保留HealthDataCenter 迁移到管理员视图内部
**代码骨架**:
```typescript
import { useDashboardRole } from '../../hooks/useDashboardRole';
import { DoctorDashboard } from './StatisticsDashboard/DoctorDashboard';
import { NurseDashboard } from './StatisticsDashboard/NurseDashboard';
import { AdminDashboard } from './StatisticsDashboard/AdminDashboard';
import { OperatorDashboard } from './StatisticsDashboard/OperatorDashboard';
const DASHBOARD_MAP = {
doctor: DoctorDashboard,
nurse: NurseDashboard,
admin: AdminDashboard,
operator: OperatorDashboard,
};
export default function StatisticsDashboard() {
const role = useDashboardRole();
const DashboardComponent = DASHBOARD_MAP[role];
return <DashboardComponent />;
}
```
**验证**: 各角色登录切换,确认正确渲染对应视图
---
### Phase 2 整体验证
1. `cargo test` — 后端测试通过
2. `pnpm build` — 前端构建通过
3. 启动后端 + 前端,逐角色登录验证仪表盘内容
4. 提交:`feat(web): 仪表盘角色自适应重构 + 后端个人工作量 API`
---
## Phase 3 详细步骤:列表页统一
> 每个列表页迁移遵循统一模式:
> 1. 引入 PageContainer + FilterBar
> 2. 手动 fetch 逻辑迁移到 usePaginatedData
> 3. 筛选器补充至至少 3 个维度
> 4. 列调整UUID→姓名、日期格式化、合并列、增加 Checkbox
> 5. 暗色模式统一处理
### 迁移模板(每个页面遵循)
```typescript
// 迁移前:手动 state + fetch
const [data, setData] = useState([]);
const [total, setTotal] = useState(0);
const [page, setPage] = useState(1);
const [loading, setLoading] = useState(false);
const fetchXxx = async (p = page) => { ... };
// 迁移后usePaginatedData + PageContainer
const { data, total, page, loading, searchText, setSearchText, filters, setFilters, refresh } =
usePaginatedData<XxxItem, XxxFilters>(xxxApi.list, { pageSize: 20, defaultFilters: {...} });
return (
<PageContainer title="页面标题" filters={...} onResetFilters={...} actions={...}>
<Table rowKey="id" dataSource={data} ... />
</PageContainer>
);
```
---
### Task 3.1 — 患者管理页迁移
**文件**: `apps/web/src/pages/health/PatientList.tsx`
**改动清单**:
1. 手动 state → `usePaginatedData` with filters
2. 引入 `PageContainer` + `FilterBar`
3. 筛选器:搜索(姓名/手机/身份证) + 状态 + 性别 + 注册日期范围(当前只有搜索+状态,补充性别和日期范围)
4. 列调整:
- 新增 Checkbox 列
- 合并姓名+来源 → 姓名列(`<EntityName name={record.name} />` 副标题显示手机号+来源)
- 合并认证状态 → 状态列
- 出生日期 → 年龄(`calcAge(record.birth_date)`
- 创建时间 → 最近就诊时间(`formatDateTime(record.last_visit_at)`
- UUID 列删除(如有)
5. 暗色模式:移除手动 isDark 处理,由 PageContainer 统一
---
### Task 3.2 — 医生管理页迁移
**文件**: `apps/web/src/pages/health/DoctorList.tsx`
**改动清单**:
1. 手动 state → `usePaginatedData`
2. 筛选器:搜索(姓名) + 科室 + 职称 + 在线状态
3. 列调整Checkbox + 姓名副标题显示科室+职称 + UUID→姓名 + 日期格式化
---
### Task 3.3 — 预约管理页迁移
**文件**: `apps/web/src/pages/health/AppointmentList.tsx`
**改动清单**:
1. 手动 state → `usePaginatedData`
2. 筛选器:状态 + 日期范围 + 患者搜索 + 预约类型
3. 列调整Checkbox + patient_id → `<EntityName>` + doctor_id → `<EntityName>` + 日期格式化
4. 状态操作保留现有下拉切换模式(已实现得好)
---
### Task 3.4 — 随访任务页迁移
**文件**: `apps/web/src/pages/health/FollowUpTaskList.tsx`
**改动清单**:
1. 手动 state → `usePaginatedData`
2. 筛选器:状态 + 计划日期范围 + 随访类型 + 执行人
3. 列调整patient_id → `<EntityName>` + assignee_id → `<EntityName>` + 日期格式化
4. 执行人筛选:下拉列表从医护 API 获取
---
### Task 3.5 — 咨询管理页迁移
**文件**: `apps/web/src/pages/health/ConsultationList.tsx`
**改动清单**:
1. 手动 state → `usePaginatedData`
2. 筛选器:状态 + 日期范围(当前可能缺少日期范围)
3. 列调整:日期格式化 + UUID→姓名
---
### Task 3.6 — 积分(规则/商品/订单)迁移
**文件**:
- `apps/web/src/pages/health/PointsRuleList.tsx`
- `apps/web/src/pages/health/PointsProductList.tsx`
- `apps/web/src/pages/health/PointsOrderList.tsx`
**改动清单**(每个页面):
1. 引入 `PageContainer` + `FilterBar`
2. 筛选器:
- 积分规则:类型 + 状态
- 积分商品:搜索(名称) + 类型 + 上架状态
- 积分订单:状态 + 日期范围
3. 列调整:日期格式化 + UUID→姓名
---
### Task 3.7 — 告警列表迁移
**文件**: `apps/web/src/pages/health/AlertList.tsx`
**改动清单**:
1. 引入 `PageContainer` + `FilterBar`
2. 筛选器:状态 + 严重程度 + 规则名搜索 + 日期范围
3. 列调整patient_id → `<EntityName>`(可点击跳转患者详情)
4. 日期格式化:使用 `formatRelative()` + title 悬停绝对时间
5. 替换组件内独立的 dayjs 导入为 `utils/dayjs`
---
### Task 3.8 — 文章管理页迁移
**文件**: `apps/web/src/pages/health/ArticleList.tsx`
**改动清单**:
1. 引入 `PageContainer` + `FilterBar`
2. 筛选器:搜索(标题) + 分类 + 状态
3. 列调整:日期格式化 + Checkbox
---
### Phase 3 整体验证
1. `pnpm build` — 前端构建通过
2. 逐页面打开,确认:
- PageContainer 统一布局
- 筛选器功能正常(搜索/下拉/日期范围)
- 列调整生效(无 UUID、日期格式化、EntityName 兜底)
- Checkbox 批量选择可用
- 暗色模式正确
3. 提交:`refactor(web): 列表页统一迁移至 PageContainer + 筛选/列/格式化规范`
---
## Phase 4 详细步骤:表单升级
### Task 4.1 — 患者表单Modal → Drawer + 分组
**文件**: `apps/web/src/pages/health/PatientList.tsx`(修改现有表单部分)
**改动清单**:
1. 引入 `DrawerForm` 组件
2. 创建表单改为 4 分组:
- 基本信息:姓名*、性别、出生日期、血型
- 联系方式:手机号、身份证号、地址
- 医疗信息:过敏史、备注
- 紧急联系人:姓名、电话、关系(新增字段)
3. 字段数从 7 → 12布局 columns=2
4. 编辑模式完整回填:确保 `allergy_history``notes` 字段从 `initialValues` 正确填充
5. 新增"紧急联系人"字段提交到后端(需确认 API 是否支持,如不支持先前端预留)
**关键改动点**:
```typescript
// 旧Modal + 垂直单列
<Modal open={modalOpen} ...>
<Form form={form} layout="vertical">
<Form.Item name="name" .../>
...
</Form>
</Modal>
// 新DrawerForm + 分组双列
<DrawerForm title={editingPatient ? '编辑患者' : '新建患者'}
open={modalOpen} onClose={() => setModalOpen(false)}
onSubmit={handleSubmit} initialValues={editingPatient ?? undefined}
columns={2}
sections={[
{ title: '基本信息', fields: <>{nameField}{genderField}{birthDateField}{bloodTypeField}</> },
{ title: '联系方式', fields: <>{phoneField}{idNumberField}{addressField}</> },
{ title: '医疗信息', fields: <>{allergyField}{notesField}</> },
{ title: '紧急联系人', fields: <>{emergencyNameField}{emergencyPhoneField}{emergencyRelationField}</> },
]}
/>
```
**验证**: 创建患者 → 全字段填写 → 提交成功;编辑患者 → 所有字段正确回填 → 修改提交
---
### Task 4.2 — 预约表单Drawer + 排班校验
**文件**: `apps/web/src/pages/health/AppointmentList.tsx`(修改现有表单部分)
**改动清单**:
1. 引入 `DrawerForm` 组件columns=2
2. 分组:
- 患者信息:患者选择、预约类型、日期
- 医生信息:医生选择、时段、排班状态反馈
3. 排班校验:选择医生+日期后,调用排班 API 显示可用时段
- 有空位:显示绿色"可预约 X 位"
- 无空位:显示红色"已满"并禁用提交
4. 修复当前问题:患者/医生 Select 从 Form.Item 外部受控改为 Form.Item 内部管理(解决校验问题)
5. 取消预约原因:从 Modal.confirm 改为独立 Drawer
**关键改动点**:
```typescript
// 排班校验反馈组件
function ScheduleStatus({ doctorId, date }: { doctorId: string; date: string }) {
const { data, loading } = useScheduleCheck(doctorId, date);
if (loading) return <Spin size="small" />;
if (!data?.available) return <Alert type="error" message="该时段已满" />;
return <Alert type="success" message={`可预约 ${data.available}`} />;
}
```
**验证**: 创建预约 → 选医生+日期 → 看到排班状态 → 选择时段 → 提交成功
---
### Task 4.3 — 随访填写Drawer + 完整回填
**文件**: `apps/web/src/pages/health/FollowUpTaskList.tsx`(修改现有表单部分)
**改动清单**:
1. 填写记录从 Modal → `DrawerForm`columns=1
2. 分组:
- 基本信息:患者、计划日期、随访类型(只读展示)
- 填写内容:执行人、随访结果、备注
3. 完整回填:编辑时加载已保存的随访记录数据
4. 保留现有 Modal 用于简单操作(状态变更)
**验证**: 创建随访任务 → 填写记录 → 保存 → 重新打开确认回填
---
### Task 4.4 — 积分商品Drawer + 图片上传
**文件**: 积分商品页面(修改现有表单部分)
**改动清单**:
1. 引入 `DrawerForm`columns=2
2. 分组:
- 基本信息:名称、类型、所需积分、库存数量
- 展示:图片上传、描述、上架状态
3. 图片上传:使用 Ant Design Upload 组件,支持预览和删除
4. 字段数 7→8新增描述字段如 API 支持)
**验证**: 创建商品 → 上传图片 → 提交 → 编辑确认回填
---
### Phase 4 整体验证
1. `pnpm build` — 前端构建通过
2. 每个表单执行创建 + 编辑完整流程:
- 患者表单12 字段分组双列,编辑回填
- 预约表单:排班校验反馈,时段选择
- 随访填写Drawer 完整回填
- 积分商品:图片上传
3. 提交:`feat(web): 表单升级 — Modal→Drawer + 分组双列布局`
---
## Phase 5 详细步骤:小程序重构
> 每个小程序页面独立修改,互不依赖,可按任意顺序或并行实施。
### Task 5.1 — 患者首页重设计
**文件**:
- `apps/miniprogram/src/pages/index/index.tsx`
- `apps/miniprogram/src/pages/index/index.scss`
**改动清单**:
1. 顶部区域:浅色 → 深色渐变卡片
- 集成今日健康摘要(血压/心率/血糖三格,无数据时显示引导文案)
- 右上角提醒角标(未读消息/异常提醒数)
2. 快捷服务:纯文字图标 → SVG 图标 + 白色卡片日常上报、预约挂号、在线咨询、AI报告
- 使用微信小程序 `<icon>` 组件或内嵌 SVG
3. 待办事项:增加日期卡片视觉(左侧日期圆圈 + 右侧内容)
4. 新增"健康资讯"推荐区块1-2 篇文章卡片)
5. 空状态引导:"今天还没录入数据,点击开始" + 录入按钮
---
### Task 5.2 — 健康数据 Hub 优化
**文件**:
- `apps/miniprogram/src/pages/health/index.tsx`
- `apps/miniprogram/src/pages/health/index.scss`
**改动清单**:
1. 体征卡片增加参考范围文字(如"正常: 90-140 mmHg"
2. 体征卡片增加微型趋势 sparkline纯 CSS/SVG 实现,不加载 ECharts
3. 打卡入口合并到顶部区域(当前独立卡片 → 顶部快捷操作栏)
4. 趋势链接优化为横向滚动卡片(而非当前列表)
---
### Task 5.3 — 日常监测表单分组折叠
**文件**:
- `apps/miniprogram/src/pages/daily-monitoring/index.tsx`
- `apps/miniprogram/src/pages/daily-monitoring/index.scss`
**改动清单**:
1. 8 个区块改为 3 组折叠:
- 晨间体征(收缩压/舒张压/心率)
- 晚间体征(收缩压/舒张压/心率)
- 其他(体重/血糖/出入量/体温/备注)
2. 异常值实时提示:输入超出参考范围时红框高亮 + 文字提醒
3. 提交前校验摘要弹窗:如有异常值弹出确认("血压偏高 (160/95),确认提交?"
4. 参考范围常量定义在组件顶部或 utils
---
### Task 5.4 — 预约流程优化
**文件**:
- `apps/miniprogram/src/pages/appointment/create/index.tsx`
**改动清单**:
1. 增量优化现有 `WeekCalendar` 组件:
-`available_count === 0` 的时段添加灰显样式
- 不足时显示"当日无空位"提示
2. 选择日期后高亮可约时段,不可约灰显
3. 预约详情页增加"修改预约"和"取消预约"操作按钮(如尚未有)
---
### Task 5.5 — 咨询聊天优化
**文件**:
- `apps/miniprogram/src/pages/consultation/detail/index.tsx`
**改动清单**:
1. 消息按日期分组显示("今天"、"昨天"、具体日期)
- 在消息列表中插入日期分割线
2. 图片消息支持点击预览大图(`Taro.previewImage`
3. "对方正在输入"标记为后续迭代(需后端 WebSocket
---
### Task 5.6 — 积分商城优化
**文件**: `apps/miniprogram/src/pages/points/` 相关页面
**改动清单**:
1. 顶部积分余额大字显示 + 签到按钮
2. 商品卡片增加库存提示("剩余 X 件")和兑换按钮
3. 分类 Tab 保留但优化视觉(更紧凑的胶囊 Tab
---
### Task 5.7 — 医护工作台重设计
**文件**:
- `apps/miniprogram/src/pages/doctor/index.tsx`
- `apps/miniprogram/src/pages/doctor/index.scss`
**改动清单**:
1. 顶部增加异常体征提醒横幅(与 Web 医生视图紧急提醒联动,同数据源)
2. 工作概览卡片改为 SVG 图标 + 数字 + 标签三层结构(替代当前中文字符"患"/"消"/"随"
3. 新增患者搜索入口(顶部搜索框)
4. 快捷操作增加"随访记录"、"排班查看"入口
---
### Task 5.8 — 趋势图表优化
**文件**: `apps/miniprogram/src/components/TrendChart/index.tsx`
**改动清单**:
1. ECharts 懒加载:按需加载 EcCanvas 组件(`React.lazy` + Suspense
2. 加载中显示骨架屏
3. 异常值红点标记(数据点超出参考范围时红色标记 + 点击显示详情)
4. 时间范围选择器7天/30天/90天 按钮组(替代当前可能只有单一时间范围)
---
### Phase 5 整体验证
1. `cd apps/miniprogram && pnpm build` — 小程序构建通过
2. 微信开发者工具中预览,逐页面确认改动
3. 真机扫码预览,确认样式和交互
4. 提交:`feat(miniprogram): 8 页面 UI/UX 重构 — 首页/健康/监测/预约/聊天/积分/医护/图表`
---
## Phase 6 详细步骤:验收
### Task 6.1 — 暗色模式全量测试
**范围**: Web 所有已改动页面 + 小程序
**步骤**:
1. 切换到暗色主题
2. 逐页面检查:
- PageContainer 背景色正确
- DrawerForm 内容区可读
- FilterBar 筛选控件可见
- 表格行交替色正确
- EntityName 灰色文字在暗背景上可读
3. 小程序暗色模式(如支持):顶部卡片、表单、图表
**通过标准**: 无白块、无不可读文字、无颜色冲突
---
### Task 6.2 — 响应式布局测试
**步骤**:
1. 浏览器宽度调整为 1280px / 1440px / 1920px
2. 检查仪表盘布局在各宽度下的排列:
- 双列区域是否正确折叠
- 统计卡是否响应式排列
- 表格是否水平滚动
3. 小程序:确认各页面在 iPhone SE / iPhone 15 Pro / iPad 下正常
**通过标准**: 无内容溢出、无重叠、无截断
---
### Task 6.3 — 各角色权限测试
**步骤**:
1. 创建 4 个测试账号,分别赋予 doctor / nurse / admin / operator 角色
2. 逐角色登录 Web 端:
- 仪表盘显示对应角色视图
- 各列表页筛选/操作按权限显示
- 表单创建/编辑权限正确
3. 逐角色登录小程序(医护端):
- 医护工作台显示对应内容
- 操作权限正确
**通过标准**: 每个角色只看到授权内容,无 403 误报
---
### Task 6.4 — 生产构建验证
**步骤**:
1. `cd apps/web && pnpm build` — 无 warning/error
2. `cd apps/miniprogram && pnpm build` — 无 warning/error
3. `cargo build --release` — 后端构建通过
4. 部署到测试环境,执行冒烟测试
**通过标准**: 所有构建零 error冒烟测试关键流程通过
---
## 总结
| Phase | 工期 | Task 数 | 关键产出 |
|-------|------|---------|---------|
| Phase 1 | 1-2天 | 6 | PageContainer / EntityName / DrawerForm / FilterBar / format 工具 |
| Phase 2 | 2-3天 | 7 | 4 角色仪表盘 + 个人工作量 API |
| Phase 3 | 3-4天 | 8 | 8 个列表页统一迁移 |
| Phase 4 | 2-3天 | 4 | 4 个表单升级为 Drawer + 分组 |
| Phase 5 | 3-4天 | 8 | 8 个小程序页面重构 |
| Phase 6 | 1天 | 4 | 暗色/响应式/权限/构建验收 |
| **合计** | **12-17天** | **37** | |
**依赖关系**:
- Phase 1 是所有后续 Phase 的基础
- Phase 2仪表盘与 Phase 3列表页可并行
- Phase 4表单依赖 Phase 3 完成对应页面的迁移
- Phase 5小程序独立于 Web 端,可与 Phase 2-4 并行
- Phase 6 必须在所有 Phase 完成后执行