659 lines
17 KiB
Markdown
659 lines
17 KiB
Markdown
# 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">></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` 这些命名,避免偏离计划。
|