Files
taiheEhu/docs/superpowers/plans/2026-05-08-sleep-report-page.md
2026-05-08 11:30:37 +08:00

17 KiB
Raw Blame History

Sleep Report Page Implementation Plan

For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (- [ ]) syntax for tracking.

Goal: 为当前小程序新增一个可交互的睡眠报告页,并在首页和底部导航中接入跳转入口,同时保持项目结构简单、适合新手继续维护。

Architecture: 报告页采用单页纵向滚动布局,使用本地 mock 数据驱动 日报 / 周报 / 月报、日期、房间、设备切换。纯逻辑抽到独立工具文件中,用最小测试基建验证评分等级、状态颜色和 mock 选择逻辑;页面层保持轻量组件拆分,不引入大型图表或状态管理依赖。

Tech Stack: Taro 4、React 18、TypeScript、SCSS、Node.js 内置 node:test


File Structure

  • Create: src/pages/report/index.tsx
    • 报告页主文件,负责状态切换、数据选择、页面组装和点击反馈
  • Create: src/pages/report/index.scss
    • 报告页整体样式和各个数据卡片、图表占位样式
  • Create: src/pages/report/index.config.ts
    • 报告页页面配置
  • Create: src/pages/report/mock.ts
    • 睡眠报告 mock 数据和维度、日期、房间、设备选项
  • Create: src/pages/report/report-utils.ts
    • 评分等级、状态色、mock 选择等纯逻辑
  • Create: scripts/tests/report-utils.test.cjs
    • 使用 Node 内置测试的 JS 测试文件
  • Create: tsconfig.report-tests.json
    • 仅为纯逻辑测试编译 report-utils.ts
  • Modify: package.json
    • 增加最小测试命令
  • Modify: src/app.config.ts
    • 注册报告页
  • Modify: src/pages/index/index.tsx
    • 底部导航与首页入口接入报告页跳转
  • Modify: src/pages/index/index.scss
    • 首页报告入口样式和导航点击态微调
  • Modify: README.md
    • 更新新增报告页和运行说明

Task 1: 建立可测试的报告工具函数与最小测试基建

Files:

  • Create: src/pages/report/report-utils.ts

  • Create: scripts/tests/report-utils.test.cjs

  • Create: tsconfig.report-tests.json

  • Modify: package.json

  • Step 1: 写评分和数据选择工具的失败测试

const test = require("node:test");
const assert = require("node:assert/strict");
const {
  getSleepLevel,
  getStatusTone,
  pickReportRecord
} = require("../../tmp/report-tests/report-utils.js");

test("getSleepLevel maps score 65 to 合格", () => {
  assert.deepEqual(getSleepLevel(65), {
    label: "合格",
    tone: "warning"
  });
});

test("getStatusTone maps 异常 to danger", () => {
  assert.equal(getStatusTone("异常"), "danger");
});

test("pickReportRecord falls back to first device record", () => {
  const record = pickReportRecord(
    {
      "2026-05-08": {
        roomA: {
          deviceA: { score: 65 },
          deviceB: { score: 79 }
        }
      }
    },
    "2026-05-08",
    "roomA",
    "missing-device"
  );

  assert.equal(record.score, 65);
});
  • Step 2: 运行测试并确认当前失败

Run:

npm run test:report

Expected:

  • 命令失败

  • 报错提示 report-utils.js 不存在或相关导出不存在

  • Step 3: 写最小实现与测试编译配置

export type SleepLevel = "excellent" | "good" | "warning" | "danger";

export function getSleepLevel(score: number) {
  if (score >= 90) return { label: "优秀", tone: "excellent" as const };
  if (score >= 75) return { label: "良好", tone: "good" as const };
  if (score >= 60) return { label: "合格", tone: "warning" as const };
  return { label: "异常", tone: "danger" as const };
}

export function getStatusTone(status: string) {
  if (status === "正常" || status === "优秀") return "excellent";
  if (status === "良好") return "good";
  if (status === "注意" || status === "合格") return "warning";
  return "danger";
}

export function pickReportRecord<T>(
  records: Record<string, Record<string, Record<string, T>>>,
  dateKey: string,
  roomKey: string,
  deviceKey: string
) {
  const rooms = records[dateKey] ?? {};
  const devices = rooms[roomKey] ?? {};

  if (devices[deviceKey]) {
    return devices[deviceKey];
  }

  const firstDevice = Object.values(devices)[0];

  if (!firstDevice) {
    throw new Error("未找到报告数据");
  }

  return firstDevice;
}
{
  "extends": "./tsconfig.json",
  "compilerOptions": {
    "noEmit": false,
    "outDir": "tmp/report-tests",
    "module": "CommonJS",
    "target": "ES2020",
    "declaration": false
  },
  "include": ["src/pages/report/report-utils.ts"]
}
{
  "scripts": {
    "test:report": "tsc -p tsconfig.report-tests.json && node --test scripts/tests/report-utils.test.cjs"
  }
}
  • Step 4: 再次运行测试并确认通过

Run:

npm run test:report

Expected:

  • 3 个测试通过

  • 退出码为 0

  • Step 5: 提交这一小步

git add package.json tsconfig.report-tests.json src/pages/report/report-utils.ts scripts/tests/report-utils.test.cjs
git commit -m "test: add report utils coverage"

Task 2: 注册报告页并接入首页两个入口

Files:

  • Modify: src/app.config.ts

  • Modify: src/pages/index/index.tsx

  • Modify: src/pages/index/index.scss

  • Create: src/pages/report/index.config.ts

  • Create: src/pages/report/index.tsx

  • Create: src/pages/report/index.scss

  • Step 1: 先为首页跳转行为写失败测试或可验证占位

由于当前项目没有页面级单测基础,这一步使用最小可验证方式:先在报告页文件不存在的状态下运行编译,确认新增页面前不能通过对应页面注册与引用。

Run:

npm run build:weapp

Expected:

  • 当前构建通过

  • 但还没有报告页,也不存在新的页面注册和跳转逻辑

  • Step 2: 注册报告页并在首页接入跳转

export default defineAppConfig({
  pages: ["pages/index/index", "pages/report/index"],
  window: {
    navigationBarTitleText: "新手小程序",
    navigationBarBackgroundColor: "#1AAD19",
    navigationBarTextStyle: "white",
    backgroundTextStyle: "light",
    backgroundColor: "#f6f7fb"
  }
});
const navItems = [
  { key: "home", label: "首页", active: true },
  { key: "report", label: "报告", active: false },
  { key: "assistant", label: "小e", active: false },
  { key: "message", label: "消息", active: false, badge: true },
  { key: "mine", label: "我的", active: false }
];

const openReportPage = () => {
  Taro.navigateTo({ url: "/pages/report/index" });
};

const handleTabClick = (key: string, label: string) => {
  if (key === "report") {
    openReportPage();
    return;
  }

  showToast(`${label}功能待接入`);
};
<View className="device-report-entry" onClick={openReportPage}>
  <View>
    <Text className="device-report-entry__eyebrow">昨夜睡眠分析</Text>
    <Text className="device-report-entry__title">查看睡眠报告</Text>
  </View>
  <Text className="device-report-entry__arrow">&gt;</Text>
</View>
  • Step 3: 新建最小报告页壳子
export default definePageConfig({
  navigationStyle: "custom"
});
export default function ReportPage() {
  return <View className="report-page">睡眠报告加载中</View>;
}
.report-page {
  min-height: 100vh;
  background: linear-gradient(180deg, #1e2432 0%, #191f2c 100%);
}
  • Step 4: 运行构建确认页面注册与跳转壳子可编译

Run:

npm run build:weapp

Expected:

  • 构建通过

  • 编译产物包含 pages/report

  • Step 5: 提交这一小步

git add src/app.config.ts src/pages/index/index.tsx src/pages/index/index.scss src/pages/report/index.config.ts src/pages/report/index.tsx src/pages/report/index.scss
git commit -m "feat: add sleep report page entry"

Task 3: 接入 mock 数据、筛选状态和顶部总览区

Files:

  • Create: src/pages/report/mock.ts

  • Modify: src/pages/report/index.tsx

  • Modify: src/pages/report/index.scss

  • Modify: src/pages/report/report-utils.ts

  • Step 1: 为 mock 选择逻辑补一个新的失败测试

test("pickReportRecord returns selected device when it exists", () => {
  const record = pickReportRecord(
    {
      "2026-05-08": {
        roomA: {
          deviceA: { score: 65 },
          deviceB: { score: 88 }
        }
      }
    },
    "2026-05-08",
    "roomA",
    "deviceB"
  );

  assert.equal(record.score, 88);
});
  • Step 2: 运行测试并确认新用例失败

Run:

npm run test:report

Expected:

  • 新增用例失败,提示逻辑未覆盖或实现未更新

  • Step 3: 写报告页 mock 数据、顶部状态和总览区最小实现

export const reportDimensions = ["daily", "weekly", "monthly"] as const;

export const reportOptions = {
  daily: {
    dates: ["2026-05-08", "2026-05-07"],
    rooms: [
      { label: "婴儿房", value: "roomA" },
      { label: "主卧", value: "roomB" }
    ],
    devices: [
      { label: "床垫监测仪 A1", value: "deviceA" },
      { label: "床垫监测仪 B2", value: "deviceB" }
    ]
  }
};
const [dimension, setDimension] = useState<ReportDimension>("daily");
const [dateKey, setDateKey] = useState(reportOptions.daily.dates[0]);
const [roomKey, setRoomKey] = useState(reportOptions.daily.rooms[0].value);
const [deviceKey, setDeviceKey] = useState(reportOptions.daily.devices[0].value);

const currentRecord = pickReportRecord(reportRecords[dimension], dateKey, roomKey, deviceKey);
const sleepLevel = getSleepLevel(currentRecord.score);
<View className="report-header">
  <View className="report-header__back" onClick={() => Taro.navigateBack()} />
  <View className="report-header__main">
    <Text className="report-header__name">{currentRecord.babyName}</Text>
    <Text className="report-header__date">{dateKey}</Text>
  </View>
  <View className="report-header__share" onClick={() => showToast("分享功能待接入")} />
</View>
<View className="report-score-card">
  <Text className="report-score-card__score">{currentRecord.score}</Text>
  <Text className={`report-score-card__level report-score-card__level--${sleepLevel.tone}`}>
    {sleepLevel.label}
  </Text>
</View>
  • Step 4: 运行测试和构建

Run:

npm run test:report
npm run build:weapp

Expected:

  • 报告工具测试全部通过

  • 报告页能完成顶部区、维度切换和筛选区的编译

  • Step 5: 提交这一小步

git add src/pages/report/mock.ts src/pages/report/index.tsx src/pages/report/index.scss src/pages/report/report-utils.ts scripts/tests/report-utils.test.cjs
git commit -m "feat: add report header and summary state"

Task 4: 实现评分总览、趋势图和核心统计卡片

Files:

  • Modify: src/pages/report/index.tsx

  • Modify: src/pages/report/index.scss

  • Step 1: 先为状态色映射补一个失败测试

test("getStatusTone maps 合格 to warning", () => {
  assert.equal(getStatusTone("合格"), "warning");
});
  • Step 2: 运行测试并确认失败

Run:

npm run test:report

Expected:

  • 新增状态映射用例失败

  • Step 3: 实现评分构成、主趋势图、睡眠分数趋势图和 9 项统计卡片

<View className="report-section">
  <Text className="report-section__title">睡眠质量趋势</Text>
  <View className="trend-chart">
    {currentRecord.sleepTrend.map((point, index) => (
      <View
        key={point.time}
        className={`trend-chart__point ${selectedTrendTime === point.time ? "is-active" : ""}`}
        style={{ left: `${index * 12}%`, bottom: `${point.score}%` }}
        onClick={() => setSelectedTrendTime(point.time)}
      />
    ))}
  </View>
</View>
<View className="metric-grid">
  {currentRecord.metrics.map((metric) => (
    <View className="metric-card" key={metric.label}>
      <Text className="metric-card__label">{metric.label}</Text>
      <Text className="metric-card__value">
        {metric.value}
        <Text className="metric-card__unit">{metric.unit}</Text>
      </Text>
      <Text className={`metric-card__status metric-card__status--${getStatusTone(metric.status)}`}>
        {metric.status}
      </Text>
    </View>
  ))}
</View>
  • Step 4: 运行测试与构建,确认可交互卡片和趋势点正常编译

Run:

npm run test:report
npm run build:weapp

Expected:

  • 所有测试通过

  • 报告页的评分、趋势图和统计卡片编译通过

  • Step 5: 提交这一小步

git add src/pages/report/index.tsx src/pages/report/index.scss src/pages/report/report-utils.ts scripts/tests/report-utils.test.cjs
git commit -m "feat: add report charts and metrics"

Task 5: 实现 AI 分析与深度分析模块

Files:

  • Modify: src/pages/report/index.tsx

  • Modify: src/pages/report/index.scss

  • Modify: src/pages/report/mock.ts

  • Step 1: 为缺省报告数据错误路径补一个失败测试

test("pickReportRecord throws when no room data exists", () => {
  assert.throws(
    () => pickReportRecord({}, "2026-05-08", "roomA", "deviceA"),
    /未找到报告数据/
  );
});
  • Step 2: 运行测试并确认失败

Run:

npm run test:report

Expected:

  • 新增错误路径用例失败

  • Step 3: 实现 AI 摘要、异常统计、HRV、心率散点、呼吸波形、低氧、打鼾、睡眠结构、自主神经和日间预测模块

<View className="report-section">
  <Text className="report-section__title">AI 分析</Text>
  {currentRecord.aiSummary.map((item) => (
    <View className="insight-card" key={item.title}>
      <Text className="insight-card__title">{item.title}</Text>
      <Text className="insight-card__body">{item.body}</Text>
    </View>
  ))}
</View>
<View className="report-section">
  <Text className="report-section__title">心率变异性(HRV</Text>
  {currentRecord.hrvMetrics.map((item) => (
    <View className="kv-row" key={item.label}>
      <Text>{item.label}</Text>
      <Text>{item.value}</Text>
    </View>
  ))}
</View>
<View className="report-section">
  <Text className="report-section__title">日间状态预测</Text>
  <View className="daytime-grid">
    {currentRecord.daytimePrediction.map((item) => (
      <View className={`daytime-chip daytime-chip--${getStatusTone(item.status)}`} key={item.label}>
        <Text>{item.label}</Text>
        <Text>{item.status}</Text>
      </View>
    ))}
  </View>
</View>
  • Step 4: 运行测试与构建

Run:

npm run test:report
npm run build:weapp

Expected:

  • 错误路径和已有逻辑测试通过

  • 报告页所有深度分析模块通过编译

  • Step 5: 提交这一小步

git add src/pages/report/index.tsx src/pages/report/index.scss src/pages/report/mock.ts src/pages/report/report-utils.ts scripts/tests/report-utils.test.cjs
git commit -m "feat: complete sleep report sections"

Task 6: 更新 README 并做最终验证

Files:

  • Modify: README.md

  • Modify: src/app.config.ts

  • Modify: src/pages/index/index.tsx

  • Modify: src/pages/index/index.scss

  • Modify: src/pages/report/index.tsx

  • Modify: src/pages/report/index.scss

  • Modify: src/pages/report/index.config.ts

  • Modify: src/pages/report/mock.ts

  • Modify: src/pages/report/report-utils.ts

  • Modify: package.json

  • Modify: tsconfig.report-tests.json

  • Modify: scripts/tests/report-utils.test.cjs

  • Step 1: 更新 README 的页面说明和运行命令说明

## 1. 目前已经包含什么

- 首页设备绑定业务示例页面
- 睡眠报告演示页

## 13. 你接下来最常做的开发动作

### 改报告页

编辑:

- `src/pages/report/index.tsx`
- `src/pages/report/mock.ts`
  • Step 2: 运行完整验证

Run:

npm run test:report
npm run build:weapp

Expected:

  • 报告页工具测试全部通过

  • Taro 小程序构建通过

  • 无新增依赖安装步骤

  • Step 3: 检查最终改动范围

Run:

git status --short

Expected:

  • 只出现本计划涉及的文件改动

  • Step 4: 提交最终实现

git add README.md package.json tsconfig.report-tests.json scripts/tests/report-utils.test.cjs src/app.config.ts src/pages/index/index.tsx src/pages/index/index.scss src/pages/report/index.config.ts src/pages/report/index.tsx src/pages/report/index.scss src/pages/report/mock.ts src/pages/report/report-utils.ts
git commit -m "feat: add sleep report page"

Self-Review

  • 规格要求的两处入口、顶部区域、三种时间维度、筛选区、评分总览、趋势图、统计卡片、AI 分析、深度分析模块、README 更新和验证命令都已经映射到具体任务。
  • 计划没有使用 TODOTBD类似上一步 这类占位写法。
  • 任务里的函数名、文件路径和命令保持一致,后续执行时应继续沿用 getSleepLevelgetStatusTonepickReportRecord 这些命名,避免偏离计划。