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

35 KiB
Raw Blame History

HMS UI/UX 全面重构 — 实施计划

日期: 2026-04-28 | 依赖规格: docs/superpowers/specs/2026-04-28-ui-ux-overhaul-design.md 总工期: 12-17 天 | 6 Phase · 28 Task

前置条件

  • 设计规格已通过审查
  • 角色代码映射已确认
  • 技术栈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 实例

代码骨架:

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 个函数:formatDateformatDateTimeformatRelativecalcAge

代码骨架:

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. 创建组件,接收 nameidfallbackLabel props
  2. name 存在时直接显示
  3. name 缺失时显示灰色 fallback 文字 + Tooltip 显示 ID

代码骨架:

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 布局,支持暗色模式

代码骨架:

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. 批量操作栏在有选中项时显示,替换常规操作栏

代码骨架:

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

代码骨架:

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. 数据来源:从 patientsdoctor_id 筛选)、follow_up_tasksconsultationsvital_signs 表聚合计算
  4. 注册路由 GET /api/v1/health/dashboard/personal-stats
  5. 添加 utoipa 注解

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'

代码骨架:

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 迁移到管理员视图内部

代码骨架:

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. 暗色模式统一处理

迁移模板(每个页面遵循)

// 迁移前:手动 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_historynotes 字段从 initialValues 正确填充
  5. 新增"紧急联系人"字段提交到后端(需确认 API 是否支持,如不支持先前端预留)

关键改动点:

// 旧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

关键改动点:

// 排班校验反馈组件
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 → DrawerFormcolumns=1
  2. 分组:
    • 基本信息:患者、计划日期、随访类型(只读展示)
    • 填写内容:执行人、随访结果、备注
  3. 完整回填:编辑时加载已保存的随访记录数据
  4. 保留现有 Modal 用于简单操作(状态变更)

验证: 创建随访任务 → 填写记录 → 保存 → 重新打开确认回填


Task 4.4 — 积分商品Drawer + 图片上传

文件: 积分商品页面(修改现有表单部分)

改动清单:

  1. 引入 DrawerFormcolumns=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 完成后执行