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

659 lines
17 KiB
Markdown
Raw Permalink 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.
# 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: 写评分和数据选择工具的失败测试**
```js
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:
```bash
npm run test:report
```
Expected:
- 命令失败
- 报错提示 `report-utils.js` 不存在或相关导出不存在
- [ ] **Step 3: 写最小实现与测试编译配置**
```ts
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;
}
```
```json
{
"extends": "./tsconfig.json",
"compilerOptions": {
"noEmit": false,
"outDir": "tmp/report-tests",
"module": "CommonJS",
"target": "ES2020",
"declaration": false
},
"include": ["src/pages/report/report-utils.ts"]
}
```
```json
{
"scripts": {
"test:report": "tsc -p tsconfig.report-tests.json && node --test scripts/tests/report-utils.test.cjs"
}
}
```
- [ ] **Step 4: 再次运行测试并确认通过**
Run:
```bash
npm run test:report
```
Expected:
- `3` 个测试通过
- 退出码为 `0`
- [ ] **Step 5: 提交这一小步**
```bash
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:
```bash
npm run build:weapp
```
Expected:
- 当前构建通过
- 但还没有报告页,也不存在新的页面注册和跳转逻辑
- [ ] **Step 2: 注册报告页并在首页接入跳转**
```ts
export default defineAppConfig({
pages: ["pages/index/index", "pages/report/index"],
window: {
navigationBarTitleText: "新手小程序",
navigationBarBackgroundColor: "#1AAD19",
navigationBarTextStyle: "white",
backgroundTextStyle: "light",
backgroundColor: "#f6f7fb"
}
});
```
```ts
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}功能待接入`);
};
```
```tsx
<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: 新建最小报告页壳子**
```ts
export default definePageConfig({
navigationStyle: "custom"
});
```
```tsx
export default function ReportPage() {
return <View className="report-page"></View>;
}
```
```scss
.report-page {
min-height: 100vh;
background: linear-gradient(180deg, #1e2432 0%, #191f2c 100%);
}
```
- [ ] **Step 4: 运行构建确认页面注册与跳转壳子可编译**
Run:
```bash
npm run build:weapp
```
Expected:
- 构建通过
- 编译产物包含 `pages/report`
- [ ] **Step 5: 提交这一小步**
```bash
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 选择逻辑补一个新的失败测试**
```js
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:
```bash
npm run test:report
```
Expected:
- 新增用例失败,提示逻辑未覆盖或实现未更新
- [ ] **Step 3: 写报告页 mock 数据、顶部状态和总览区最小实现**
```ts
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" }
]
}
};
```
```tsx
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);
```
```tsx
<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>
```
```tsx
<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:
```bash
npm run test:report
npm run build:weapp
```
Expected:
- 报告工具测试全部通过
- 报告页能完成顶部区、维度切换和筛选区的编译
- [ ] **Step 5: 提交这一小步**
```bash
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: 先为状态色映射补一个失败测试**
```js
test("getStatusTone maps 合格 to warning", () => {
assert.equal(getStatusTone("合格"), "warning");
});
```
- [ ] **Step 2: 运行测试并确认失败**
Run:
```bash
npm run test:report
```
Expected:
- 新增状态映射用例失败
- [ ] **Step 3: 实现评分构成、主趋势图、睡眠分数趋势图和 9 项统计卡片**
```tsx
<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>
```
```tsx
<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:
```bash
npm run test:report
npm run build:weapp
```
Expected:
- 所有测试通过
- 报告页的评分、趋势图和统计卡片编译通过
- [ ] **Step 5: 提交这一小步**
```bash
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: 为缺省报告数据错误路径补一个失败测试**
```js
test("pickReportRecord throws when no room data exists", () => {
assert.throws(
() => pickReportRecord({}, "2026-05-08", "roomA", "deviceA"),
/未找到报告数据/
);
});
```
- [ ] **Step 2: 运行测试并确认失败**
Run:
```bash
npm run test:report
```
Expected:
- 新增错误路径用例失败
- [ ] **Step 3: 实现 AI 摘要、异常统计、HRV、心率散点、呼吸波形、低氧、打鼾、睡眠结构、自主神经和日间预测模块**
```tsx
<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>
```
```tsx
<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>
```
```tsx
<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:
```bash
npm run test:report
npm run build:weapp
```
Expected:
- 错误路径和已有逻辑测试通过
- 报告页所有深度分析模块通过编译
- [ ] **Step 5: 提交这一小步**
```bash
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 的页面说明和运行命令说明**
```md
## 1. 目前已经包含什么
- 首页设备绑定业务示例页面
- 睡眠报告演示页
## 13. 你接下来最常做的开发动作
### 改报告页
编辑:
- `src/pages/report/index.tsx`
- `src/pages/report/mock.ts`
```
- [ ] **Step 2: 运行完整验证**
Run:
```bash
npm run test:report
npm run build:weapp
```
Expected:
- 报告页工具测试全部通过
- Taro 小程序构建通过
- 无新增依赖安装步骤
- [ ] **Step 3: 检查最终改动范围**
Run:
```bash
git status --short
```
Expected:
- 只出现本计划涉及的文件改动
- [ ] **Step 4: 提交最终实现**
```bash
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 更新和验证命令都已经映射到具体任务。
- 计划没有使用 `TODO``TBD``类似上一步` 这类占位写法。
- 任务里的函数名、文件路径和命令保持一致,后续执行时应继续沿用 `getSleepLevel``getStatusTone``pickReportRecord` 这些命名,避免偏离计划。