Merge commit 'a6382d6'

# Conflicts:
#	README.md
#	src/app.config.ts
#	src/pages/index/index.scss
#	src/pages/index/index.tsx
This commit is contained in:
czz
2026-05-08 11:39:05 +08:00
27 changed files with 3708 additions and 4 deletions

View File

@@ -0,0 +1,658 @@
# 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` 这些命名,避免偏离计划。